This chapter contains a description of the formal requirements or standards
regarding coding that you should adhere to when you develop TYPO3
extensions or Core parts.
This chapter goes into details about automatic testing: Writing,
maintaining and running them in various scopes.
Introduction
TYPO3 is a content management system based on PHP.
The TYPO3 - Getting Started Tutorial gives you an
introduction to the core concepts of TYPO3 and will help you to kickstart your
first project.
A basic TYPO3 installation
Installation with Composer is covered in the Getting Started Guide
(see Installation chapter).
If you think you have found a bug in the TYPO3 Core code, have a look at our
issue tracker on forge
where you can check if the bug has already been reported or not.
API A-Z
The TYPO3 Core code serves a dual purpose: It functions both as an out-of-the-box
application and as a library providing APIs for extensions that enhance projects
with additional functionality.
The TYPO3 Core itself organizes its code into extensions as well, with the "core"
extension offering the majority of API classes. These classes are utilized by
other key extensions such as "frontend" and "backend". These three extensions
are mandatory for any TYPO3-based project, while others, like "scheduler,"
are optional.
This chapter focuses on the APIs primarily provided by these three essential
extensions.
TYPO3 APIs are primarily documented within the source code itself. Maintaining
documentation in multiple locations is impractical due to the frequent changes
in the codebase. This chapter highlights the most critical elements of the
API.
The TYPO3 component responsible for rendering the HTML and adding assets to a
TYPO3 frontend or backend page is called
\TYPO3\CMS\Core\Page\PageRenderer .
The
PageRenderer collects all assets to be rendered, takes care of
options such as concatenation or compression and finally generates the necessary
tags.
There are multiple ways to add assets to the
PageRenderer in TYPO3.
For configuration options via TypoScript (usually used for the main theme files),
see the TypoScript Reference. In
extensions, both directly using the
PageRenderer as well as using the
more convenient
AssetCollector is possible.
Asset collector
With the
\TYPO3\CMS\Core\Page\AssetCollector class, CSS and JavaScript
code (inline or external) can be added multiple times, but rendered only once in
the output. The class may be used directly in PHP code or the assets can be
added via the
<f:asset.css> and
<f:asset.script> ViewHelpers.
The
priority flag (default:
false) controls where the asset is
inserted:
JavaScript will be output inside
<head> if
$priority == true,
or at the bottom of the
<body> tag if
$priority == false.
CSS will always be output inside
<head>, yet grouped by
$priority.
The asset collector helps to work with content elements as components,
effectively reducing the CSS to be loaded. It takes advantage of HTTP/2, which
removes the necessity to concatenate all files in one file.
The asset collector class is implemented as a singleton
(
\TYPO3\CMS\Core\SingletonInterface ). It replaces various other existing
options in TypoScript and methods in PHP for
inserting JavaScript and CSS code.
The asset collector also collects information about images on a page,
which can be used in cached and non-cached components.
New in version 13.3
Option external to skip URL processing in AssetRenderer has been added.
The
AssetCollector option external can be used for
asset files using
$assetCollector->addStyleSheet()
or
$assetCollector->addJavaScript(). If set all processing of the asset
URI (like the addition of the cache busting parameter) is skipped and the input
path will be used as-is in the resulting HTML tag.
The API
classAssetCollector
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.
One dimensional hash map (array with non-numerical keys) with scalar values
Returns
self
removeJavaScript(string $identifier)
param $identifier
the identifier
Returns
self
removeInlineJavaScript(string $identifier)
param $identifier
the identifier
Returns
self
removeStyleSheet(string $identifier)
param $identifier
the identifier
Returns
self
removeInlineStyleSheet(string $identifier)
param $identifier
the identifier
Returns
self
removeMedia(string $identifier)
param $identifier
the identifier
Returns
self
getMedia()
Returns
array
getJavaScripts(?bool $priority = NULL)
param $priority
the priority, default: NULL
Returns
array
getInlineJavaScripts(?bool $priority = NULL)
param $priority
the priority, default: NULL
Returns
array
getJavaScriptModules()
Returns
array
getStyleSheets(?bool $priority = NULL)
param $priority
the priority, default: NULL
Returns
array
getInlineStyleSheets(?bool $priority = NULL)
param $priority
the priority, default: NULL
Returns
array
hasJavaScript(string $identifier)
param $identifier
the identifier
Returns
bool
hasInlineJavaScript(string $identifier)
param $identifier
the identifier
Returns
bool
hasStyleSheet(string $identifier)
param $identifier
the identifier
Returns
bool
hasInlineStyleSheet(string $identifier)
param $identifier
the identifier
Returns
bool
hasMedia(string $fileName)
param $fileName
the fileName
Returns
bool
Note
If the same asset is registered multiple times using different attributes or
options, both sets are merged. If the same attributes or options are given
with different values, the most recently registered ones overwrite the
existing ones. The
has methods can be used to check if an asset
exists before generating it again, hence avoiding redundancy.
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'
The property
additionalHeaderData has been marked as internal
and should not be used. Use
AssetCollector->addJavaScript() instead
(like described in the examples above).
Deprecated since version 13.4
The class
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
and its global instance
$GLOBALS['TSFE'] have been marked as
deprecated. The class will be removed with TYPO3 v14.
The user session contains all information for logged in backend and
frontend users, as well as anonymous visitors without login. It can be
used to store session data like a shopping basket.
Enhance your TYPO3 security with Multi-Factor Authentication (MFA),
adding an extra layer of protection beyond passwords for safer logins.
Backend user API
In TYPO3, backend users (BE users) are responsible for managing content,
settings, and administration tasks within the backend. They are stored in the
be_users database table and authenticated via the
Backend user object
stored in the global variable
$GLOBALS['BE_USER']
(class
\TYPO3\CMS\Core\Authentication\BackendUserAuthentication ).
Sudo mode (step-up authentication) for password changes
New in version 12.4.32 / 13.4.13
This functionality was introduced in response to security advisory TYPO3-CORE-SA-2025-013
to mitigate password-change risks.
This mechanism prevents unauthorized password changes if an administrator
session is hijacked or left unattended.
When an administrator edits their own user account or changes the
password of another user via the admin interface, password confirmation
(step-up authentication) is required.
Step-up authentication requires the administrator to re-enter their password
Note
This may pose challenges when integrating remote single sign-on (SSO)
providers, as these typically do not support a dedicated step-up
authentication process.
In such cases, you can use the PSR-14 events SudoModeRequiredEvent
(triggered before showing the sudo-mode verification dialog) and
SudoModeVerifyEvent
(triggered before actually verifying the submitted password) to adapt the behavior.
Frontend user API
In TYPO3, frontend users (FE users) are responsible for accessing restricted
content and personalized areas of a TYPO3 website. They are stored in the
fe_users database table
and authenticated via the frontend.user request attribute
(class
\TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication ).
The system extension
typo3/cms-felogin
offers a plugin that can
included in the page so that frontend users can login and logout. It also
features a password-forgotten workflow.
The TYPO3 Core does not provide a plugin to register frontend users. There are
multiple third-party extensions available in the TER, for example
evoweb/sf-register
.
Writes session data so it is available in the next request.
Example: Save shopping basket into user session
Let us assume we have an Extbase Controller for a shopping basket. We want to
preserve the data the user enters before closing the browser
window. This should also work for non logged-in users.
We can use
$this->request->getAttribute('frontend.user')
to create a frontend user on the fly if none exists.
If no Frontend user
is currently logged in, an anonymous frontend user will be created on the fly.
In the browser of the current user a session cookie will be set linking
them to the anonymous frontend user.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Controller;
usePsr\Http\Message\ResponseInterface;
useTYPO3\CMS\Extbase\Mvc\Controller\ActionController;
useTYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
classShoppingCartControllerextendsActionController{
publicconst FORM_SESSION = 'myextension_cart';
publicfunctionputItemAction(Item $item): ResponseInterface{
// Fetch cart from session or create a new one
$cart = $this->getCartFromSession() ?? new Cart();
$cart->add($item);
$this->storeCartInSession($cart);
return$this->redirect('list');
}
publicfunctionlist(): ResponseInterface{
$this->view->assign('cart', $this->getCartFromSession());
return$this->htmlResponse();
}
privatefunctiongetFrontendUser(): FrontendUserAuthentication{
// This will create an anonymous frontend user if none is logged inreturn$this->request->getAttribute('frontend.user');
}
privatefunctionstoreCartInSession(Cart $cart): void{
// We use type ses to store the data in the session$this->getFrontendUser()->setKey('ses', self::FORM_SESSION, serialize($cart));
// Important: store session data! Or it is not available in the next request!$this->getFrontendUser()->storeSessionData();
}
privatefunctiongetCartFromSession(): ?Cart{
$data = $this->getFrontendUser()->getKey('ses', self::FORM_SESSION);
if (is_string($data)) {
$cart = unserialize($data);
if ($cart instanceof Cart) {
return $cart;
}
}
returnnull;
}
}
Copied!
User session management
User sessions in TYPO3 are represented as
UserSession objects. The TYPO3
authentication service chain creates or updates user sessions
when authenticating users.
The
UserSession object contains all information regarding a user's session,
for website visitors with session data (e.g. basket for anonymous / not-logged-in users),
for frontend users as well as authenticated backend users. These are for example,
the session id, the session data, if a session was updated, if the session is anonymous,
or if it is marked as permanent and so on.
The
UserSession object can be used to change and
retrieve information in an object-oriented way.
For creating
UserSession objects the
UserSessionManager must be used
since this manager acts as the main factory for user
sessions and therefore handles all necessary tasks like fetching, evaluating
and persisting them. It effectively encapsulates all calls to the
SessionManager which is used for the
session backend.
Public API of
UserSessionManager
The
UserSessionManager can be retrieved using its static factory
method
create():
useTYPO3\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:
classUserSessionManager
Fully qualified name
\TYPO3\CMS\Core\Session\UserSessionManager
The purpose of the UserSessionManager is to create new user session objects (acting as a factory),
depending on the need / request, and to fetch sessions from the session backend, effectively
encapsulating all calls to the SessionManager.
The UserSessionManager can be retrieved using its static factory method create():
useTYPO3\CMS\Core\Session\UserSessionManager;
$loginType = 'BE'; // or 'FE' for frontend
$userSessionManager = UserSessionManager::create($loginType);
Creates and returns a session from the given request. If the given
$cookieName can not be obtained from the request an anonymous
session will be returned.
param $request
the request
param $cookieName
Name of the cookie that might contain the session
Return description
An existing session if one is stored in the cookie, an anonymous session otherwise
Returns
\UserSession
createAnonymousSession()
Creates and returns an anonymous session object (which is not persisted)
Regenerates the given session. This method should be used whenever a
user proceeds to a higher authorization level, for example when an
anonymous session is now authenticated.
param $sessionId
The session id
param $existingSessionRecord
If given, this session record will be used instead of fetching again, default: []
param $anonymous
If true session will be regenerated as anonymous session, default: false
Updates the session timestamp for the given user session if the session
is marked as "needs update" (which means the current timestamp is
greater than "last updated + a specified grace-time").
param $session
the session
Return description
A modified user session with a last updated value if needed
Creates a UserSessionManager instance for the given login type. Has
several optional arguments used for testing purposes to inject dummy
objects if needed.
Ideally, this factory encapsulates all TYPO3_CONF_VARS options, so
the actual object does not need to consider any global state.
param $loginType
the loginType
param $sessionLifetime
the sessionLifetime, default: NULL
param $sessionManager
the sessionManager, default: NULL
param $ipLocker
the ipLocker, default: NULL
Returns
static
setLogger(\Psr\Log\LoggerInterface $logger)
Sets a logger.
param $logger
the logger
Public API of
UserSession
The session object created or retrieved by the
UserSessionManager
provides the following API methods:
classUserSession
Fully qualified name
\TYPO3\CMS\Core\Session\UserSession
Represents all information about a user's session.
A user session can be bound to a frontend / backend user, or an anonymous session based on session data stored
in the session backend.
If a session is anonymous, it can be fixated by storing the session in the backend, but only if there
is data in the session.
if a session is user-bound, it is automatically fixated.
The $isNew flag is meant to show that this user session object was not
fetched from the session backend, but initialized in the first place by
the current request.
The $data argument stores arbitrary data valid for the user's session.
A permanent session is not issued by a session-based cookie but a
time-based cookie. The session might be persisted in the user's browser.
getIdentifier()
Return description
The session ID. This is the ses_id respectively the AbstractUserAuthentication->id
Returns
string
getUserId()
Return description
The user ID the session belongs to. Can also return 0 or NULL Which indicates an anonymous session. This is the ses_userid.
Returns
?int
getLastUpdated()
Return description
The timestamp of the last session data update. This is the ses_tstamp.
Returns
int
set(string $key, ?mixed $value)
Sets or updates session data value for a given $key. It is also
internally used if calling AbstractUserAuthentication->setSessionData()
param $key
The key whose value should be updated
param $value
The value or NULL to unset the key
hasData()
Checks whether the session has data assigned
Returns
bool
get(string $key)
Returns the session data for the given $key or NULL if the key does
not exist. It is internally used if calling
AbstractUserAuthentication->getSessionData()
param $key
the key
getData()
Return description
The whole data array.
Returns
array
overrideData(array $data)
Overrides the whole data array. Can also be used to unset the array.
This also sets the $wasUpdated pointer to true
param $data
the data
dataWasUpdated()
Checks whether the session data has been updated
Returns
bool
isAnonymous()
Checks if the user session is an anonymous one. This means, the
session does not belong to a logged-in user
Returns
bool
getIpLock()
Return description
The ipLock state of the session
Returns
string
isNew()
Checks whether the session is marked as new
Returns
bool
isPermanent()
Checks whether the session was marked as permanent
Verifies and resolves the session ID from a submitted cookie value:
Cookie: <JWT(HS256, [identifier => <session-id>], <signature(encryption-key, cookie-domain)>)>
param $cookieValue
submitted cookie value
param $scope
the scope
Return description
Session ID, null in case verification failed
Returns
non-empty-string|null
Session storage framework
TYPO3 comes with the option to choose between different storages for both
frontend and backend user sessions (called session backends).
The Core ships two session backends by default:
Database storage
Redis storage
By default user sessions are stored in the database using the database
storage backend.
Database storage backend
The database storage backend only requires two configuration options:
The table name (table option) and whether anonymous sessions (has_anonymous option) may be stored.
The default configuration used for sessions by the Core is:
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.
Tip
If a Redis instance is running on the same machine as the webserver
the hostname 'localhost' can be used.
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):
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.
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
Note
For the Reports module to be visible, the system extension
reports has to be installed. You can install it via Composer:
composer require typo3/cms-reports
Copied!
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.
Note
When no session exists, the authentication process is triggered
by a login request. In the frontend, this happens when a form field
called logintype is submitted with value login. The same
happens for the backend, but with a form field called login_status.
JSON Web Tokens (JWT) are used to transport user
session identifiers in be_typo_user and fe_typo_user cookies.
Using JWT's HS256 (HMAC signed based on SHA256) allows to determine whether a
session cookie is valid before comparing with server-side stored session data.
This enhances the overall performance a bit, since sessions cookies would be
checked for every request to TYPO3's backend and frontend.
The session cookies can be pre-validated without querying the database, which
can filter invalid requests and might improve overall performance a bit.
As a consequence session tokens are not sent "as is", but are wrapped in a
corresponding JWT message, which contains the following payload:
identifier reflects the actual session identifier
time reflects the time of creating the cookie (RFC 3339 format)
The login data
There is a typical set of data that is transmitted to authentication
service in order to enable them to do their work:
uname
This is the user name. This can be whatever makes sense for the
available authentication services. For the default service, this
will match data from the "username" column of the "be_users" or
"fe_users" table for BE or FE authentication respectively.
uident
This is the password, possibly encrypted.
uident_text
This is the clear text value of the password. If the password is
originally submitted in clear text, both "uident" and "uident_text"
contain the same value.
Inside an authentication service, this data is available in
$this->login.
The "auth" services API
The services of type "auth" are further divided into subtypes,
which correspond to various steps in the authentication process.
Most subtypes exist for both FE and BE and are differentiated
accordingly.
To each subtype corresponds a part of the "auth" services public
API. They are listed below in the order in which they are called
during the authentication process.
processLoginDataBE, processLoginDataFE
This subtype performs preprocessing on the submitted
login data.
The method to implement is
processLoginData().
It receives as argument the login data and the password
transmission strategy (which corresponds to the login security
level, where only 'normal' can be used.
It returns the boolean value
true,
when the login data has been successfully processed .
It may also return a numerical value equal to 200 or greater,
which indicates that no further login data processing should
take place (see The service chain).
In particular, this subtype is implemented by the TYPO3 Core
AuthenticationService, which trims the given login data.
getUserFE, getUserBE
This subtype corresponds to the operation of searching in the
database if the credentials that were given correspond to an
existing user. The method to implement is
getUser().
It is expected to return an array containing the user information
or
false if no user was found.
authUserFE, authUserBE
This subtype performs the actual authentication based on the
provided credentials. The method to implement is
authUser().
It receives the user information (as returned by
getUser())
as an input and is expected to return a numerical value,
which is described later.
Note
Before any of the above-mentioned methods are called, the authentication
process will call the
initAuth() method of each service. This
sets up a lot of data for the service. It also makes it possible to
override part of the default settings with
service-specific options.
This represents very advanced tuning and is not described here.
Please refer to
\TYPO3\CMS\Core\Authentication\AbstractAuthenticationService::initAuth()
to learn more about the possibilities offered during authentication services
initialization.
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:
Warning
Previously, there was an error in the documentation. It did not match
the actual behaviour. This has now been fixed. For details, see
forge#91993.
a negative value or 0 (<=0) indicates that the authentication has
definitely failed and that no other "auth" service should be
called up.
a value larger than 0 and smaller than 100 indicates that the authentication
was successful, but that further services should also perform their
own authentication.
a value of 100 or more (>= 100) indicates that the user was not authenticated,
this service is not responsible for the authentication and that further
services should authenticate.
a value of 200 or more (>=200) indicates that the authentication was successful
and that no further tries should be made by other services down
the chain.
auth failed
auth success
no auth
continue
1..99
100..199
stop
<= 0
>= 200
For "getUserFE" and "getUserBE" subtypes, the logic is reversed.
The service chain will stop as soon as one user is found.
Developing an authentication service
Use the Service API to implement your service class.
When developing your own "auth" services, the chances are high
that you will want to implement only the "getUser*" and "authUser*"
subtypes.
There are several public extensions providing such services, so you should
be able to find examples to inspire and guide you. Anyway authentication
services can be very different from one another, so it wouldn't make much
sense to try and provide an example in this manual.
One important thing to know is that the TYPO3 authentication
process needs to have users inside database records ("fe_users" or
"be_users"). This means that if you interface with a third-party
server, you will need to create records on the TYPO3 side. It is
up to you to choose whether this process happens on the fly (during
authentication) or if you want to create an import process (as a
Scheduler task, for example) that will synchronize users between
TYPO3 and the remote system.
Note
You probably do not want to store the actual password of imported
users in the TYPO3 database. It is recommended to store
an arbitrary string in such case, making sure that such string
is random enough for security reasons. TYPO3 provides method
\TYPO3\CMS\Core\Crypto\Random::generateRandomHexString()
which can be used for such a purpose.
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:
This could be used in a scenario where users go through a login portal
and then choose to access the TYPO3 backend, for example. In such a case
we would want the users to be automatically authenticated, but would not
need to repeat the process upon each request.
The authentication process can also be forced to go through
all services for the "getUser*" subtype by setting:
for BE or FE respectively. This will collect all possible users rather than
stopping at the first one available.
CSRF-like request token handling
New in version 12.0
A CSRF-like request token handling is available to mitigate potential cross-site
requests on actions with side effects. This approach does not require an
existing server-side user session, but uses a nonce as a "pre-session". The
main scope is to ensure a user actually has visited a page, before submitting
data to the webserver.
This token can only be used for HTTP methods POST, PUT or PATCH, but
for instance not for GET request.
The
\TYPO3\CMS\Core\Middleware\RequestTokenMiddleware resolves
request tokens and nonce values from a request and enhances responses with
a nonce value in case the underlying application issues one. Both items are
serialized as a JSON Web Token (JWT) hash signed with HS256. Request tokens
use the provided nonce value during signing.
Session cookie names involved for providing the nonce value:
typo3nonce_[hash] in case request served with plain HTTP
__Secure-typo3nonce_[hash] in case request served with secured HTTPS
Submitting request token value to application:
HTTP body, for example in <form> via parameter __RequestToken
HTTP header, for example in XHR via header X-TYPO3-Request-Token
Attention
When working with multiple browser tabs, an existing nonce value (stored as
session cookie in the browser of the user) might be overridden.
Note
The current concept uses the
\TYPO3\CMS\Core\Security\NoncePool which
supports five different nonces in the same request. The pool purges nonces
15 minutes (900 seconds) after they have been issued.
This happens on the previous legitimate visit on a page that offers
a corresponding form that shall be protected. The RequestToken and Nonce
objects (later created implicitly in this example) are organized in the
\TYPO3\CMS\Core\Context\SecurityAspect .
Invoke action request and provide nonce and request token values
When submitting the form and invoking the corresponding action, same-site
cookies typo3nonce_[hash] and request-token value __RequestToken are
sent back to the server. Without using a separate nonce in a scope that is
protected by the client, the corresponding request token could be easily
extracted from markup and used without having the possibility to verify the
procedural integrity.
The middleware
\TYPO3\CMS\Core\Middleware\RequestTokenMiddleware
takes care of providing the received nonce and received request token values
in
\TYPO3\CMS\Core\Context\SecurityAspect . The handling controller
action needs to verify that the request token has the expected
'my/process' scope.
<?phpuseTYPO3\CMS\Core\Context\Context;
useTYPO3\CMS\Core\Context\SecurityAspect;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyController{
publicfunctionshowFormAction(){
// for the implementation, see above
}
publicfunctionprocessAction(){
$context = GeneralUtility::makeInstance(Context::class);
$securityAspect = SecurityAspect::provideIn($context);
$requestToken = $securityAspect->getReceivedRequestToken();
if ($requestToken === null) {
// No request token was provided in the request// for example, (overridden) templates need to be adjusted
} elseif ($requestToken === false) {
// There was a request token, which could not be verified with the nonce// for example, when nonce cookie has been overridden by another HTTP request
} elseif ($requestToken->scope !== 'my/process') {
// There was a request token, but for a different scope// for example, when a form with different scope was submitted
} else {
// The request token was valid and for the expected scope$this->doTheMagic();
// The middleware takes care to remove the cookie in case no other// nonce value shall be emitted during the current HTTP requestif ($requestToken->getSigningSecretIdentifier() !== null) {
$securityAspect->getSigningSecretResolver()->revokeIdentifier(
$requestToken->getSigningSecretIdentifier(),
);
}
}
}
}
TYPO3 includes a password policy validator which can be used to validate
passwords against configurable password policies. A default password policy is
included which ensures that passwords meet the following requirements:
At least 8 characters
At least one number
At least one upper case character
At least one special character
It must be different than current password (if available)
Password policies can be configured individually for both frontend and backend
context. It is also possible to extend a password policy with custom validation
requirements.
The password policy applies to:
Creating a backend user during installation
Setting a new password for a backend user in User settings
Resetting a password for a backend user
Resetting a password for a frontend user
Password fields in tables
be_users and
fe_users
Optionally, a password policy can be configured for custom TCA fields of the
type password.
Configuring password policies
A password policy is defined in the TYPO3 global configuration. Each policy
must have a unique identifier (the identifier default is reserved by TYPO3)
and must at least contain one validator.
The password policy identifier is used to assign the defined password policy
to the backend and/or frontend context. By default, TYPO3 uses the
password policy default:
This validator can be used to ensure, that the new user password is not
equal to the old password. The validator must always be configured with
the exclude action
\TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction::NEW_USER_PASSWORD,
because it should be excluded, when a new user account is created.
To disable the password policy globally (e.g. for local development) an empty
string has to be supplied as password policy for frontend and backend context:
if (\TYPO3\CMS\Core\Core\Environment::getContext()->isDevelopment()) {
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = '';
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = '';
}
Copied!
Warning
Do not deactivate the password policies on a production server as this
decreases security massively. In the example above the deactivation of
the password policies is wrapped into a condition which is only applied
in development context.
Custom password validator
To create a custom password validator, a new class has to be added which
extends
\TYPO3\CMS\Core\PasswordPolicy\Validator\AbstractPasswordValidator .
It is required to overwrite the following functions:
public function initializeRequirements(): void
public function validate(string $password, ?ContextData $contextData = null): bool
Please refer to
\TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator
for a detailed implementation example.
Tip
The third-party extension
derhansen/add_pwd_policy
provides additional
password validators. It can also be used as a resource for writing your
own password validator.
Validate a password manually
You can use the
\TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator to validate a
password using the validators configured in
$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .
In the following example a command to generate
a public-private key pair validates the password from user input against the
default policy of the current TYPO3 installation.
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
"something to know" (= the password) and
"something to own" (= an authenticator device, or an authenticator app
on mobile phones or desktop devices).
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.
Note
Currently, TYPO3 provides the integration for the TYPO3 backend. It is
planned to support multi-factor authentication for the frontend in the
future.
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.
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.
Note
The default provider setting will be automatically applied on activation of
the first provider, or in case it is the recommended provider for this user.
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.
Note
All of these deactivate buttons are executed immediately, after
confirming the dialog, and cannot be undone.
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.
Do not require multi-factor authentication (default)
1
Require multi-factor authentication for all users
2
Require multi-factor authentication only for non-admin users
3
Require multi-factor authentication only for admin users
To set this requirement only for a specific user or user group, a user TSconfig
option auth.mfa.required <t3tsref:user-auth-mfa-required> is available.
The user TSconfig option overrules the global configuration.
auth.mfa.required = 1
Copied!
Allowed provider
It is possible to only allow a subset of the available providers for some users
or user groups.
A configuration option "Allowed multi-factor authentication providers" is
available in the user groups record in the "Access List" tab.
There may be use cases in which a single provider should be
disallowed for a specific user, which is configured to be allowed in
one of the assigned user groups. Therefore, the user TSconfig option
auth.mfa.disableProviders can
be used. It overrules the configuration from the "Access List": if a provider is
allowed in "Access List" but disallowed via user TSconfig, it will be disallowed
for the user or user group the TSconfig applies to.
This does not affect the remaining allowed providers from the "Access List".
To set a recommended provider on a per user or user group basis, the user
TSconfig option auth.mfa.recommendedProvider
can be used, which overrules the global configuration.
auth.mfa.recommendedProvider = 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 configurationMyVendor\MyExtension\Authentication\Mfa\MyProvider:tags:-name:mfa.provideridentifier:'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'
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 configurationTYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider:~
Copied!
You can also register multiple providers:
EXT:my_extension/Configuration/Services.yaml
services:# Place here the default dependency injection configurationMyVendor\MyExtension\Authentication\Mfa\MyFirstProvider:tags:-name:mfa.provideridentifier:'my-provider-1'title:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.title'description:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.description'setupInstructions:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.setupInstructions'icon:'tx-myextension-provider1-icon'MyVendor\MyExtension\Authentication\Mfa\MySecondProvider:class:TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvidertags:-name:mfa.provideridentifier:'my-provider-2'title:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.title'description:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.description'setupInstructions:'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.setupInstructions'icon:'tx-myextension-provider2-icon'# Important so that this provider acts as a fallbackdefaultProviderAllowed:truebefore:'recovery-codes'# Execute after the primary totpafter:'totp'
Copied!
The
MfaProviderInterface contains a lot of methods to be implemented by
the providers. This can be split up into state-providing ones, for example,
isActive() or
isLocked(), and functional ones, for example,
activate() or
update().
Their exact task is explained in the corresponding PHPDoc of the interface files
and the Core MFA provider implementations.
All of these methods are receiving either the current
PSR-7 request object, the
\TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager or both.
The
MfaProviderPropertyManager can be used to retrieve and update the
provider-specific properties and also contains the
getUser() method,
providing the current user object.
To store provider-specific data, the MFA API uses a new database field
mfa,
which can be freely used by the providers. The field contains a JSON-encoded
array with the identifier of each provider as array key. Common properties of
such provider array could be active or lastUsed. Since the information is
stored in either the
be_users or the
fe_users table, the context
is implicit. Same goes for the user the providers deal with. It is important to
have such a generic field so providers are able to store arbitrary data, TYPO3
does not need to know about.
To retrieve and update the providers data, the already mentioned
MfaProviderPropertyManager, which is automatically passed to all
necessary provider methods, should be used. It is highly discouraged
to directly access the
mfa database field.
Autoloading
The class autoloader takes care of finding classes in TYPO3.
About
makeInstance()
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() is a generic way
throughout Core and extensions to create objects. It takes care of singleton
and XCLASS handling.
Note
When dependent services are injected via Dependency Injection, there is no need for
makeInstance():
Injecting a different object is done by configuration - that's what
dependency injection is for.
A developer can instantiate classes using
makeInstance() - if dependency
injection cannot be used. There are some situations where
new is used
over
makeInstance(), effectively dropping especially the direct ability
to XCLASS:
Data transfer objects are often created with
new. A good example are
PSR-14 events: The calling class creates a data
transfer object that is hand over to the consumer. These DTOs must never be
changed by an extension, since they are a contract both caller and consumer
must stick to. They are thus created using
new to prevent XCLASSing.
Structures with a dedicated API that allows own implementations on a
configuration level sometimes do not use
makeInstance: Many Core
constructs come with an API to allow custom classes by dedicated
configuration. Those implement a factory pattern to deal with this. An
example is the PSR-15 middleware stack.
Autoloading classes
There is one autoloader used, the one of Composer. No matter if you run TYPO3 in
Composer mode or not (Classic mode), TYPO3 uses the Composer autoloader to
resolve all class file locations.
Loading classes with Composer mode
In Composer mode, the autoloader checks for (classmap and PSR-4) autoloading
information inside your extension's composer.json. If you do not provide
this, the autoloader falls back to the classmap autoloading like in
non-Composer mode.
Troubleshooting
Dump the class loading information manually via
composer dumpautoload
Copied!
and check that the autoload information is updated. Typically you would check
vendor/composer/ to hold files like autoload_classmap.php and
autoload_psr4.php, etc..
Check if you find the required class names in these files and install any missing extensions.
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:
This method is failsafe unless the autoload information cannot be written. In
this case, check the Install Tool for warnings and make sure thattypo3temp/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.
Tip
PSR-4 is a standard that has been developed by the PHP Framework Interop
Group (FIG). PSR-4 is an advanced standard for autoloading PHP classes and
replaces PSR-0. If you want to know more about the PHP FIG in general and
PSR-4 in specific, please visit https://www.php-fig.org/psr/psr-4/.
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.
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!
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
Note
There's no other level between admin and ordinary users.
This seems to be a strong limitation, especially
when you consider that ordinary users may not access TypoScript
templates.
However, there is a security reason for this. From a TypoScript template,
you can call a PHP script. So - in effect - a user with access to
TypoScript can run arbitrary PHP code on the server, for example
in order to create an admin account for himself. This type of escalation
cannot be allowed.
Location of users and groups
Since both backend users and backend groups are represented by records
in the database, they are edited just as any other record in the
system. However backend users and groups are configured to exist
only in the root of the page tree where only admin users have
access:
Users and groups reside on the root page
Records located in the page tree root are identified by having their
"pid" fields set to zero. The "pid" field normally contains the
relation to the page where a record belongs. Since no pages can have
the id of zero, this is the id of the root. Notice that only "admin"
users can edit records in the page root!
If you need non-admin users to create new backend users, have a look
at the TYPO3 system extension sys_action for a possible solution.
Password reset functionality
TYPO3 backend users can reset their password if they use the default TYPO3 login
mechanism.
To display the reset link on the backend login page, the following criteria must
be met:
The user has a password entered previously (indicating that no third-party
login has been used).
The user has a valid email address added to their user record.
The user is neither deleted nor disabled.
The email address is used only once for all backend users of the instance.
Once the user has entered their email address, an email is sent with a link that
allows to set a new password, which must consist of at least eight characters.
The link is valid for 2 hours and a token is added to the link. If the password
is entered correctly, it will be updated for the user and they can log in.
New in version 12.1
The new password that the user specifies must comply with the configured
password policy for the backend.
New in version 12.3
The username of the backend user is displayed in the password recovery
email alongside the reset link.
New in version 13.0
A new array variable
{userData} has been added to the password
recovery
FluidEmail object. It contains the values of all fields
belonging to the affected frontend user.
Notes on security
When having multiple users with the same email address, no reset
functionality is provided.
No information disclosure is built-in, so if the email address is not in the
system, it is not disclosed to the outside.
Rate limiting is enabled so that three emails can be sent per email address
within 30 minutes.
Tokens are stored in the database but hashed again just like the password.
When a user has logged in successfully (for example, because they remembered
the password), the token is removed from the database, effectively
invalidating all existing email links.
Implications of displaying the username in the email
A third-party gaining access to the email account has all information needed
to log in into the TYPO3 backend and potentially cause damage to the
website.
Without the username a third-party could only reset the password of the
TYPO3 backend user, but not log in, if the username is different from the
email address.
It is also possible to override the ResetRequested email templates
to remove the username and customize the result.
Both options can be configured in the Admin Tools > Settings module
or in the Install Tool, but can also be set manually via
config/system/settings.php or config/system/additional.php.
Reset password for user
Administrators can reset a user's password. This is useful primarily for
security reasons, so that an administrator does not have to send a password over
to a user in plain text (for example, by email).
The backend URL is necessary to generate the correct links to the TYPO3
instance from CLI context.
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:
Identify the roles you need; Developer, Administrator, Editor, Super
User, User, ... etc.
Configure a group for each role: attribute permissions needed to
fulfill each role.
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.
Access lists are defined at group-level. Usage of access lists for defining
user rights is described in chapter Setting up user group permissions.
The various access lists are described here for reference, with
additional technical details, where necessary.
Note
Access list don't apply to admin users. As mentioned before, admin
users have access to every single feature of the TYPO3 CMS backend.
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.
Note
This is the only access list that is also available for definition
at user-level.
This section is only available with activated
dashboard system extension.
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.
All tables that are allowed for modification (see below) are
also allowed for read access, so no need to select them in this
list as well.
Tables for editing
This is exactly the same list of tables as before, but for granting
modification rights.
Page types
TYPO3 CMS defines a number of page types. A user can be restricted
to access only some of them.
For a full discussion on page types, please refer to the
page types chapter.
Excludefields
When defining column tables in TCA, it is possible to set the
"exclude" property to "1".
This ensures that the field is hidden to users by default.
Access to it must be explicitly granted in this access list.
Explicitly allow/deny field values
When a field offers a list of options to select from, it is possible
to tell TYPO3 CMS that access to these options is restricted and should
be granted explicitly. Such fields and their values appear here.
By default users can edit records regardless of what language they are assigned to.
Using this list it is possible to restrict users to working only in selected
languages.
When a user is a member of more than one group, the access lists for
the groups are "added" together.
Mounts
TYPO3 CMS natively supports two kinds of hierarchical tree structures:
the page tree (typically visible in the Web module) and the folder
tree (typically visible in the File module). Each tree is
generated based on the mount points configured for the current user. So a
page tree is drawn from the DB Mounts which are one or more page ids
telling the Core from which "start page" to draw the tree(s). Likewise
is the folder tree drawn based on file mounts configured for the user.
DB mounts (page mounts) are set by pointing out the
page that should be mounted for the user (at user or group-level):
The DB mounts for group "Editors"
This is what the user will see:
Warning
A DB mount will appear only if the page permissions
allow the user at least read access to the mounted page (and subpages).
Otherwise nothing will appear at all!
File Mounts are a little more difficult to set up, as they
involve several steps. First of all, you need to have at least
one File Storage. By
default, you will always have one, pointing
to the fileadmin directory. It is created by TYPO3 CMS
upon installation.
Note
The fileadmin directory is the default place where
TYPO3 CMS expects media resources to be located. It can be
changed using the global configuration option
$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'].
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:
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.
Note
Here "Page content" means all records related to that page,
except other pages.
Page permissions are set and viewed with the module System > Permissions
module:
The Access module and its overview of page rights and owners
Editing permissions is described in details in chapter
Page permissions.
A user must be "admin" or the owner of a page in order to edit its
permissions.
When a user creates new pages in TYPO3 CMS they will by default get the
creating user as owner. The owner group will be set to the first
listed user group configured for the users record (if any). These defaults
can be changed through page TSconfig.
User TSconfig
User TSconfig is a hierarchical configuration structure entered in
plain text TypoScript. It can be used by all kinds of applications
inside of TYPO3 CMS to retrieve customized settings for users which
relates to a certain module or part. The options available are
described in the document TSconfig .
Other Options
This chapter presents a few more, miscellaneous options for
backend users and groups.
Backend users
Default language
This is the language in which the backend will be localized for the
user. The users can change the language themselves in the
User Settings
module.
Note
Language packs must be downloaded using the Admin Tools > Maintenance > Manage Language Packs
module. As long as the language packs are not available, the backend
will still display in English.
Fileoperation permissions
This is a complement to the File Mounts and defines exactly which operations
the user is allowed to perform on both files and folders.
Access options
A backend user can be disabled (first flag in the "General" tab). A disabled
user cannot log into the backend anymore. Furthermore, in the "Access" tab
a start and end time can be given, defining a time interval during which the
user will be allowed to log into the backend. Authentication before the start
time and after the end time will automatically fail.
Lock to domain
This setting constrains the user to use a specific domain for logging
into the TYPO3 backend. This is very useful in setups with multiple
sites.
Backend Groups
Disable
Setting this flag will immediately disable the group for
all members
Lock to domain
This restricts a group to a given domain. If a user logs in from another
domain, that group membership will be ignored.
Hide in lists
This flag will prevent the group from appearing in various
listings in TYPO3. This includes modules like System > Access.
Inherit settings from groups (Sub Groups)
Assigns sub-groups to this group. Sub-groups are
evaluated before the group including them. If a user is a member of
a group which includes one or more sub-groups, the user will also be
a member of the sub-groups.
More about file mounts
File mounts require a little more description of the concepts provided
by TYPO3. All files are handled by an application layer called
the "File Abstraction Layer" (FAL). You can find more information
about the basic concepts of FAL.
The FAL is comprised of the following components:
Drivers
Drivers are what makes it possible to access a given type of media
storage. The Core provides a driver to access the local file system.
Extensions exist that provide a driver for remote systems, like SFTP
or platforms like Amazon S3.
Storages
A file storage uses a driver to connect to a given storage system.
It is defined by a path pointing into that storage system. There can
be several storages using the same driver and pointing to different
"directories". The storage configuration depends on the driver it uses.
Thanks to the storage and its driver, the user is able to browse
files from within the TYPO3 backend as if they were stored locally.
File mounts
As discussed before, a file mount is the element which is used to
actually give access to users to some directories. A file mount is always
related to a storage.
Create a new file mount
To create a new file mount go to the module File > Filelist and create the
folder for the mount if it didn't exist yet. Then open the context menu on that
folder and choose New File mount, then give the new file mount a name.
The entry point is already set.
It is also possible to create a file mount manually in the List
module by creating a record of type
Filemount. In this case you have
to choose the storage and folder manually.
Paths for local driver storage
The file storages based on the "local file system" driver have an
option for relative or absolute paths.
The paths options for a storage based on the local file system driver
"Relative" means that the given path is relative to the fileadmin/
folder (or whatever other folder was configured using
$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'].
Absolute paths are full paths starting at the root of the file system
(i.e. / on Unix systems).
Attention
If the fileadmin/ location is not changed, be aware
that all files beneath it will be accessible via a browser,
since this directory is located below the web root. This is
perfectly fine in most cases, and indeed generally a desired
behaviour. Just be careful what kind of files you store in that
place, as they will be publicly accessible.
Absolute paths outside of the web root
must be explicitly declared in the global configuration option
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']. Any absolute
path that you want to declare in a file storage needs to have its
first part match the value of $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']
(or of the web root, which can be retrieved with \TYPO3\CMS\Core\Core\Environment::getPublicPath()).
As an example, let's say you want to define two storages, one
pointing to /home/foo/bar and one pointing to
/home/foo/baz. You could declare
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] to be
equal to /home/foo/.
Home directories
TYPO3 CMS also features the concept of "home directories". These are paths
that are automatically mounted if they are present at a path
configured in the global configuration. Thus they don't need to have a file
mount record representing them - they just need a properly named
directory to be present.
Note
That last sentence is important. The directories need to exist.
They are not created automatically.
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:
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:
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:
where only the first mount was explicitly assigned to that user.
A different icon visually distinguishes automatic file mounts.
Note
If the $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'] option is
also used, it is appended to the user home directory name. Thus a value
of _uploads would mean that our home directories become
/path/to/web/root/fileadmin/user_homes/3_uploads/
or /path/to/web/root/fileadmin/user_homes/3_editor_uploads/.
This does not apply to group home directories.
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).
This chapter describes the API that can be used to create custom backend modules
in extensions. See the following chapter for a tutorial on how to create
custom backend modules.
The PSR-14 BeforeModuleCreationEvent allows extension authors
to manipulate the module configuration before it is used to create and
register the module.
Registration of backend modules was changed with version 12. If you are
using an older version of TYPO3 please use the version switcher on the top
left of this document to go to the respective version.
The Configuration/Backend/Modules.php configuration files are
read and processed when building the container. This
means the state is fixed and cannot be changed at runtime.
If the module should be a submodule, the parent identifier, for example web
has to be set here. Have a look into the
list of available toplevel modules.
Extensions can add additional parent modules, see
Toplevel modules.
path
path
Type
string
Default
/module/<mainModule>/<subModule>
Define the path to the default endpoint. The path can be anything, but
will fallback to the known /module/<mainModule>/<subModule> pattern,
if not set.
access
access
Type
string
Can be user (editor permissions), admin, or systemMaintainer.
workspaces
workspaces
Type
string
Can be * (= always), live or offline. If not set, the value of the
parent module - if any - is used.
position
position
Type
array
The module position. Allowed values are top and bottom as
well as the key value pairs before => <identifier> and
after => <identifier>.
Path to a module icon (Deprecated: Use
iconIdentifier
instead)
labels
labels
Type
array of strings or string
An
array with the following keys:
title
description
shortDescription
The value of each array entry can either be a string containing the static text, or a locallang label reference.
Alternatively define the path of a locallang file reference.
A referenced file should contain the following label keys:
mlang_tabs_tab (used as module title)
mlang_labels_tabdescr (used as module description)
mlang_labels_tablabel (used as module short description)
component
component
Type
string
Default
TYPO3/CMS/Backend/Module/Iframe
The view component, responsible for rendering the module.
navigationComponent
navigationComponent
Type
string
Changed in version 13.1
@typo3/backend/page-tree/page-tree-element has been renamed to
@typo3/backend/tree/page-tree-element. Using old navigation
ID will trigger a PHP deprecation warning.
The module navigation component. The following are provided by the Core:
Whether the module should use the parents navigation component.
This option defaults to
true and can therefore be used to
stop the inheritance for submodules.
moduleData
moduleData
Type
array
All properties of the module data object
that may be overridden by
GET /
POST parameters of the request
get their default value defined here.
Example
Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
List of identifiers that are aliases to this module. Those are added as route
aliases, which allows to use them for building links, for example with the
\TYPO3\CMS\Backend\Routing\UriBuilder . Additionally, the aliases can
also be used for references in other modules, for example to specify a
module's parent.
Examples
Example for a new module identifier:
Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?phpdeclare(strict_types=1);
return [
'workspaces_admin' => [
'parent' => 'web',
// ...// choose the previous name or an alternative name'aliases' => ['web_WorkspacesWorkspaces'],
],
];
Copied!
Example for a route alias identifier:
Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
Generic side information that will be merged with each generated
\TYPO3\CMS\Backend\Routing\Route::$options array. This can be
used for information, that is not relevant for a module aspect,
but more relevant for the routing aspect, for example
sudo mode.
Define the routes to this module. Each route requires at least the target.
The _default route is mandatory, except for modules which can fall back
to a submodule. The path of the _default route is taken from the
top-level configuration. For all other routes, the route identifier is taken
as path, if not explicitly defined. Each route can define any
controller/action pair and can restrict the allowed HTTP methods:
Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
All subroutes are automatically registered in a
\TYPO3\CMS\Core\Routing\RouteCollection . The full syntax for route
identifiers is <module_identifier>.<sub_route>, for example,
my_module.edit. Therefore, using the
\TYPO3\CMS\Backend\Routing\UriBuilder to create a link to such a
sub-route might look like this:
Using these Extbase configurations tells the Core to bootstrap Extbase and expecting
controllers based on
\TYPO3\CMS\Extbase\Mvc\Controller\ActionController .
Do not use it for non-Extbase controller. Use routes
instead.
The extension name in UpperCamelCase for which the module is registered. If
the extension key is my_example_extension the extension name would be
MyExampleExtension.
controllerActions
controllerActions
Type
array
Define the controller action pair. The array keys are the
controller class names and the values are the actions, which
can either be defined as array or comma-separated list:
Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
The modules define explicit routes for each controller/action combination,
as long as the
enableNamespacedArgumentsForBackend feature
toggle is turned off (which is the default). This effectively means
human-readable URLs, since the controller/action combinations are no longer
defined via query parameters, but are now part of the path.
The route identifier of corresponding routes is registered with similar
syntax as standard backend modules:
<module_identifier>.<controller>_<action>. Above configuration will
therefore register the following routes:
web_ExtkeyExample
web_ExtkeyExample.MyModule_list
web_ExtkeyExample.MyModule_detail
Debug the module configuration
All registered modules are stored as objects in a registry. They can be viewed
in the backend in the System > Configuration > Backend Modules
module.
Exploring registered Backend Modules in the Configuration module
The ModuleProvider API allows extension
authors to work with the registered modules.
Backend modules with sudo mode
You can configure the sudo mode in your backend module like this:
This allows administrators to define access permissions via the module
access logic for those modules individually. It also allows to influence the
position of the third-level module.
Example
Registration of an additional third-level module for the
Web > Template module in the Configuration/Backend/Modules.php
file of an extension:
The following toplevel modules are provided by the Core:
web: Web
All modules requiring a page tree by default. These modules are mostly used
to manage content that should be displayed in the frontend.
site: Site Management
Settings for the complete site such as redirects and site settings.
file: File
All modules requiring a file system tree such as modules dealing with file
metadata, uploading etc.
tools: Admin Tools
By convention modules in this toplevel section should only be
available for admins with system maintainer rights. Therefore the
configuration array of a module displayed here should always have the
following key-value pair:
'access' => 'systemMaintainer'.
In this toplevel section modules that deal with installing and updating the Core
and extensions are available. System-wide settings are also found here.
system: System
By convention, modules in this toplevel section should only be
accessible by admins. Therefore the configuration array of a module
displayed here should always have the following key-value pair:
'access' => 'admin'.
In this toplevel section modules are situated that deal with backend user
rights or might reveal security relevant data.
The backend user interface is essentially driven by the "backend"
system extension and extended by many other system extensions.
It is divided into the following main areas:
An overview of the visual structure of the backend
Top bar
The top bar is always present. It is itself divided into two
areas: the logo and top bar tools.
The logo can be changed using the
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['backend']['backendLogo'] option.
Additional top bar tools can be registered using
$GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'] .
Module menu
This is the main navigation. All modules are structured in main
modules (which can be collapsed) and submodules which is where
the action really happens.
The module menu can be opened or closed by using the icon on the top left.
In the TYPO3 world, "module" is typically used for
the backend. Extension components which add features in the frontend
are referred to as "plugins".
Navigation frame
Any backend module may have a navigation frame or not. This frame
will typically display the page tree or the folder tree, but
custom navigation frames are possible.
The current location (i.e. page or frame) is carried over between
navigation frames when changing modules. This means, for example, that
when you move from the Web > Page module to the
Web > List module, the same page stays selected in the page tree.
DocHeader
This part is always located above the Content area. It will generally
contain a drop-down menu called the "Function menu", which allows to
navigate into the various functions offered by the module. When editing
it will also contain all the buttons necessary for saving, closing or
reverting. It may contain additional buttons for shortcuts
or any specific feature needed by the module.
Content area
This is the actual work area. Any information to show or content
to edit will be displayed here.
Contextual menus
(Right) clicking on record icons will often reveal a contextual menu.
New functions can be added to the contextual menus, but the
mechanisms vary: the page tree behaves differently than the
rest of the backend.
A typical contextual menu appears when clicking on a record icon
DocHeaderComponent
The
\TYPO3\CMS\Backend\Template\Components\DocHeaderComponent can be
used to display a standardized header section in a backend module with buttons,
menus etc. It can also be used to hide the header section in case it is
not desired to display it.
The module header displayed by the DocHeaderComponent
The
\TYPO3\CMS\Backend\Module\ModuleData object contains the user
specific module settings, for example whether the clipboard is shown,
for the requested module. Those settings
are fetched from the user's session. A PSR-15 middleware automatically
creates the object from the stored user data and attaches it to the PSR-7 Request.
The
\TYPO3\CMS\Backend\Module\ModuleData object is available as
attribute of the PSR-7 Request - in case a TYPO3 backend module is requested -
and contains the stored module data, which might have been overwritten
through the current request (with
GET /
POST).
Through the module registration one can define, which properties can
be overwritten via
GET /
POST and their default value.
The whole determination is done before the requested route target - usually
a backend controller - is called. This means, the route target can
read the final module data.
The allowed properties are defined with their default value in the
module registration:
The
ModuleData object provides the following methods:
Method
Parameters
Description
createFromModule()
$module
$data
Create a new object for the given module, while
overwriting the default values with
$data.
getModuleIdentifier()
Returns the related module identifier
get()
$propertyName
$default
Returns the value for
$propertyName, or the
$default, if not set.
set()
$propertyName
$value
Updates
$propertyName with the given
$value.
has()
$propertyName
Whether
$propertyName exists.
clean()
$propertyName
$allowedValues
Cleans a single property by the given allowed
list and falls back to either the default value
or the first allowed value.
cleanUp()
$allowedData
$useKeys
Cleans up all module data, which are defined in
the given allowed data list. Usually called with
$MOD_MENU in a controller with module menu.
toArray()
Returns the module data as
array.
In case a controller needs to store changed module data, this can still be done
using
$backendUser->pushModuleData('my_module', $this->moduleData->toArray());.
Note
It is possible to store and retrieve arbitrary module data. The
definition of
moduleData in the module registration only defines,
which properties can be overwritten in a request (with
GET /
POST).
To restrict the values of module data properties, the given
ModuleData
object can be cleaned, for example, in a controller:
If
ModuleData contains
property, the value is checked
against the
$allowedValues list. If the current value is valid,
nothing happens. Otherwise the value is either changed to the default
or if this value is also not allowed, to the first allowed value.
ModuleInterface
The registered backend modules are stored as objects in a registry and can be
fetched using the \TYPO3\CMS\Backend\Module\ModuleProvider.
All module objects implement the
\TYPO3\CMS\Backend\Module\ModuleInterface .
The
ModuleInterface basically provides getters for the options
defined in the module registration and additionally provides methods for
relation handling (main modules and sub modules).
The internal name of the module, used for referencing in permissions etc
Returns
string
getPath()
Return the main route path
Returns
string
getIconIdentifier()
The icon identifier for the module
Returns
string
getTitle()
The title of the module, used in the menu
Returns
string
getDescription()
A longer description, common for the "About" section with a long explanation
Returns
string
getShortDescription()
A shorter description, used when hovering over a module in the menu as title attribute
Returns
string
isStandalone()
Useful for main modules that are also "clickable" such as the dashboard module
Returns
bool
getComponent()
Returns the view component responsible for rendering the module (iFrame or name of the web component)
Returns
string
getNavigationComponent()
The web component to be rendering the navigation area
Returns
string
getPosition()
The position of the module, such as [top] or [bottom] or [after => anotherModule] or [before => anotherModule]
Returns
array
getAppearance()
Returns a modules appearance options, e.g. used for module menu
Returns
array
getAccess()
Can be user (editor permissions), admin, or systemMaintainer
Returns
string
getWorkspaceAccess()
Can be "*" (= empty) or "live" or "offline"
Returns
string
getParentIdentifier()
The identifier of the parent module during registration
Returns
string
getParentModule()
Get the reference to the next upper menu item
Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
hasParentModule()
Can be checked if the module is a "main module"
Returns
bool
hasSubModule(string $identifier)
Checks whether this module has a submodule with the given identifier
param $identifier
the identifier
Returns
bool
hasSubModules()
Checks if this module has further submodules
Returns
bool
getSubModule(string $identifier)
Return a submodule given by its full identifier
param $identifier
the identifier
Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
getSubModules()
Return all direct descendants of this module
Returns
\ModuleInterface[]
getDefaultRouteOptions()
Returns module related route options - used for the router
Returns
array
getDefaultModuleData()
Get allowed and available module data properties and their default values.
Returns
array
getAliases()
Return a list of identifiers that are aliases to this module
Returns
array
ModuleProvider
The
ModuleProvider API allows extension authors to work with the
registered modules.
This API is the central point to retrieve modules, since it
automatically performs necessary access checks and prepares specific structures,
for example for the use in menus.
ModuleProvider API
classModuleProvider
Fully qualified name
\TYPO3\CMS\Backend\Module\ModuleProvider
This is the central point to retrieve modules from the ModuleRegistry, while
performing the necessary access checks, which ModuleRegistry does not deal with.
isModuleRegistered(string $identifier)
Simple wrapper for the registry, which just checks if a
module is registered. Does NOT perform any access checks.
Return the requested (main) module if exist and allowed, prepared
for menu generation or similar structured output (nested). Takes
TSConfig into account. Does not respect "appearance[renderInModuleMenu]".
Backend controllers should use ModuleTemplateFactory::create()
to create instances of a
\TYPO3\CMS\Backend\Template\ModuleTemplate .
API functions of the
ModuleTemplate can be used to add buttons to
the button bar. It also implements the
\TYPO3\CMS\Core\View\ViewInterface
so values can be assigned to it in the actions.
classModuleTemplate
Fully qualified name
\TYPO3\CMS\Backend\Template\ModuleTemplate
A class taking care of the "outer" HTML of a module, especially
the doc header and other related parts.
assign(string $key, ?mixed $value)
Add a variable to the view data collection.
param $key
the key
param $value
the value
Returns
self
assignMultiple(array $values)
Add multiple variables to the view data collection.
param $values
the values
Returns
self
render(string $templateFileName = '')
Render the module.
param $templateFileName
the templateFileName, default: ''
Returns
string
renderResponse(string $templateFileName = '')
Render the module and create an HTML 200 response from it. This is a
lazy shortcut so controllers don't need to take care of this in the backend.
param $templateFileName
the templateFileName, default: ''
Returns
\Psr\Http\Message\ResponseInterface
setBodyTag(string $bodyTag)
Set to something like '<body id="foo">' when a special body tag is needed.
param $bodyTag
the bodyTag
Returns
self
setTitle(string $title, string $context = '')
Title string of the module: "My module · Edit view"
param $title
the title
param $context
the context, default: ''
Returns
self
getDocHeaderComponent()
Get the DocHeader. Can be used in controllers to add custom
buttons / menus / ... to the doc header.
ModuleTemplate by default uses queue 'core.template.flashMessages'. Modules
may want to maintain an own queue. Use this method to render flash messages
of a non-default queue at the default position in module HTML output. Call
this method before adding single messages with addFlashMessage().
param $flashMessageQueue
the flashMessageQueue
Returns
self
setUiBlock(bool $uiBlock)
UI block is a spinner shown during browser rendering phase of the module,
automatically removed when rendering finished. This is done by default,
but the UI block can be turned off when needed for whatever reason.
In many backend modules all actions should have the same module header.
So it is useful to initialize the backend module template in a function commonly
used by all actions:
The backend module of an extension can be configured via TypoScript.
The configuration is done
in
module.tx_<lowercaseextensionname>_<lowercasepluginname> or
in
module.tx_<lowercaseextensionname>.
If the part
_<lowercasepluginname> is omitted, then the setting is used
for all backend modules of that extension.
Even in the backend the TypoScript setup is used. The
settings should be done globally and not changed on a per-page basis.
Therefore they are usually set in the file
EXT:my_extension/ext_typoscript_setup.typoscript.
When accessing modules in the Admin Tools via backend user interface,
currently logged in backend users have to confirm their user password again
in order to get access to the modules in this section.
As an alternative, it is also possible to use the install
tool password. This is done
in order to mitigate unintended modifications that might occur as result
of for example possible cross-site scripting vulnerabilities in the system.
Authentication in for sudo mode in extensions using the auth service
Albeit default local authentication mechanisms are working well, there are
side effects for 3rd party extensions that make use of these auth service
chains as well - such as multi-factor authentication or single sign-on handling.
As an alternative, it is possible to confirm actions using the Install Tool
password, instead of confirming with user's password (which might be handled
with separate remote services).
Services that extend authentication with custom additional factors (2FA/MFA)
are advised to intercept only valid login requests instead of all authUser
invocations.
<?phpnamespaceMyVendor\MyExtension\Authentication;
useTYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
classMyAuthenticationServiceextendsAbstractAuthenticationService{
publicfunctionauthUser(array $user){
// only handle actual login requestsif (($this->login['status'] ?? '') !== 'login') {
// skip this service, hand over to next in chainreturn100;
}
// ...// usual processing for valid login requests// ...return0;
}
}
Copied!
Custom backend modules requiring the sudo mode
In general, the configuration for a particular route or module looks like this:
group (optional): if given, grants access to other objects of the same group
without having to verify sudo mode again for a the given lifetime. Example:
Admin Tool modules Maintainance and Settings are configured with the same
systemMaintainer group - having access to one (after sudo mode verification)
grants access to the other automatically.
lifetime: enum value of
\TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime ,
defining the lifetime of a sudo mode verification, afterwards users have to go through
the process again - cases are veryShort (5 minutes), short (10 minutes),
medium (15 minutes), long (30 minutes), veryLong (60 minutes)
All simplified classnames below are located in the namespace
\TYPO3\CMS\Backend\Security\SudoMode\Access).
The low-level request orchestration happens in the middleware
\TYPO3\CMS\Backend\Middleware\SudoModeInterceptor ,
markup rendering and payload processing in controller
\TYPO3\CMS\Backend\Controller\Security\SudoModeController .
A backend route is processed, that requires sudo mode for route URI /my/route
in
\TYPO3\CMS\Backend\Http\RouteDispatcher .
Using
AccessFactory
and
AccessStorage ,
the
\RouteDispatcher
tries to find a valid and not expired
AccessGrant item
for the specific
RouteAccessSubject('/my/route') aspect in the
current backend user session data.
In case no
AccessGrant can be
determined, a new
AccessClaim
is created for the specific
RouteAccessSubject
instance and temporarily persisted in the current user session data -
the claim also contains the originally requested route as
ServerRequestInstruction
(a simplified representation of a
\ServerRequestInterface).
Next, the user is redirected to the user interface for providing either
their own password, or the global install tool password as alternative.
Given, the password was correct, the
AccessClaim
is "converted" to an
AccessGrant ,
which is only valid for the specific subject (URI /my/route)
and for a limited lifetime.
JavaScript in TYPO3 backend
Some third-party JavaScript libraries are packaged with the TYPO3 source code.
The TYPO3 backend itself relies on quite a lot of JavaScript to do its job.
The topic of this chapter is to present how to use JavaScript properly
with TYPO3, in particular in the backend. It presents the most important
APIs in that regard.
Starting with TYPO3 v12.0 JavaScript ES6 modules may be used instead of
AMD modules, both in backend and frontend context.
JavaScript node-js style path resolutions are managed by
import maps, which allow web
pages to control the behavior of JavaScript imports.
In November 2022 import maps are supported natively by Google Chrome,
a polyfill is available for Firefox and Safari and included by TYPO3 Core
and applied whenever an import map is emitted.
For security reasons, import map configuration is only emitted when the modules
are actually used, that means when a module has been added to the current
page response via
PageRenderer->loadJavaScriptModule() or
JavaScriptRenderer->addJavaScriptModuleInstruction().
Exposing all module configurations is possible via
JavaScriptRenderer->includeAllImports(), but that should only be
done in backend context for logged-in users to avoid disclosing installed
extensions to anonymous visitors.
Configuration
A simple configuration example for an extension that maps
the Public/JavaScript folder to an import prefix @vendor/my-extensions:
<?phpreturn [
// required import configurations of other extensions,// in case a module imports from another package'dependencies' => ['backend'],
'imports' => [
// recursive definiton, all *.js files in this folder are import-mapped// trailing slash is required per importmap-specification'@vendor/my-extension/' => 'EXT:my_extension/Resources/Public/JavaScript/',
],
];
Copied!
Complex configuration example containing recursive-lookup exclusions,
third-party library definitions and overwrites:
<?phpreturn [
'dependencies' => ['core', 'backend'],
'imports' => [
'@vendor/my-extension/' => [
'path' => 'EXT:my_extension/Resources/Public/JavaScript/',
// Exclude files of the following folders from being import-mapped'exclude' => [
'EXT:my_extension/Resources/Public/JavaScript/Contrib/',
'EXT:my_extension/Resources/Public/JavaScript/Overrides/',
],
],
// Adding a third party package'thirdpartypkg' => 'EXT:my_extension/Resources/Public/JavaScript/Contrib/thidpartypkg/index.js',
'thidpartypkg/' => 'EXT:my_extension/Resources/Public/JavaScript/Contrib/thirdpartypkg/',
// Overriding a file from another package'@typo3/backend/modal.js' => 'EXT:my_extension/Resources/Public/JavaScript/Overrides/BackendModal.js',
],
];
Copied!
Loading ES6
A module can be added to the current page response either via
PageRenderer or as
JavaScriptModuleInstruction via
JavaScriptRenderer:
No ES6 JavaScript files are created directly in the TYPO3 Core. JavaScript
is created as TypeScript module which is then converted to ES6 JavaScript
during the build process. However, TypeScript and ES6 are quite similar, you
can therefore look into those files for reference. The TypeScript files can be
found on GitHub at
Build/Sources/TypeScript.
For examples of an ES6 JavaScript file have a look at the JavaScript example in
the LinkHandler Tutorial or
the example in the Notification API.
In the TYPO3 Core usage of jQuery is eliminated step-by-step as the necessary
functionality is provided by native JavaScript nowadays.
If you still have to use jQuery in your third-party extension, include it
with the following statement:
import $ from'jquery';
Copied!
Add JavaScript modules to import map in backend form
The JavaScript module import map is static and only generated and
loaded in the first request to a document. All possible future
modules requested in later Ajax calls need to be registered already
in the first initial request.
The tag
backend.form is used to identify
JavaScript modules that can be used within backend forms. This
ensures that the import maps are available for these modules
even if the element is not displayed directly.
A typical use case for this is an InlineRelationRecord where the
CKEditor is not part of the main record but needs to be loaded for
the child record.
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:
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 {
returnthis;
}
// Define the element's template
render() {
return html`<p>Hello ${this.value}!</p>`;
}
}
The modal API provided by the module
@typo3/backend/modal.js has been
adapted to be backed by a custom web component and therefore gained an updated,
stateless interface. See also section Migration.
Actions that require a user's attention must be visualized by modal windows.
TYPO3 provides an API as basis to create modal windows with severity
representation. For better UX, if actions (buttons) are attached to the modal,
one button must be a positive action. This button should get a btnClass
to highlight it.
Modals should be used rarely and only for confirmations. For information that
does not require a confirmation
the Notification API (flash message) should be used.
For complex content, like forms or a lot of information, use normal pages.
API
Changed in version 12.0
The return type of all
Modal.* factory methods has been changed from
JQuery to
ModalElement.
Callback that is triggered on button click - either a simple function or
DeferredAction /
ImmediateAction
active
active
Type
bool
Marks the button as active. If true, the button gets the focus.
btnClass
btnClass
Type
string
The CSS class for the button.
Changed in version 12.0
The
Button property dataAttributes has been removed without
replacement, as the functionality can be expressed via
Button.name
or
Button.trigger and is therefore redundant.
Data Attributes
It is also possible to use
data attributes to trigger a modal,
for example on an anchor element, which prevents the default behavior.
data-title
The title text for the modal.
data-content
The content text for the modal.
data-severity
The severity for the modal, default is info (see
TYPO3.Severity.*).
data-href
The target URL, default is the
href attribute of the element.
data-button-close-text
Button text for the close/cancel button.
data-button-ok-text
Button text for the ok button.
class="t3js-modal-trigger"
Marks the element as modal trigger.
data-static-backdrop
Render a static backdrop to avoid closing the modal when clicking it.
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(() => {
returnnew AjaxRequest('/any/endpoint').post({});
})
}
]);
Copied!
Activating any action disables all buttons in the modal. Once the action is
done, the modal disappears automatically.
Buttons of the type
DeferredAction render a spinner on activation
into the button.
A modal with static backdrop:
import Modal from'@typo3/backend/modal.js';
Modal.advanced({
title: 'Hello',
content: 'This modal is not closable via clicking the backdrop.',
size: Modal.sizes.small,
staticBackdrop: true
});
Copied!
Templates, using the HTML class
.t3js-modal-trigger to initialize
a modal dialog are also able to use the new option by adding the
data-static-backdrop attribute to the corresponding element.
<buttonclass="btn btn-default t3js-modal-trigger"data-title="Hello"data-content="This modal is not closable via clicking the backdrop."data-static-backdrop>
Open modal
</button>
Copied!
Migration
Given the following fully-fledged example of a modal that uses custom buttons,
with custom attributes, triggers and events, they should be migrated away
from
JQuery to
ModalElement usage.
The title of the slide. Will be shown as header of the slide.
content
content
Type
string|JQuery|Element|DocumentFragment
Required
true
The content of the slide. If string any HTML will be escaped. To
prevent that, chose one of the other allowed types:
JQuery:
$(`<div>Your HTML content</div>`);
Copied!
Element:
Object.assign(document.createElement('div'), {
innerHTML: 'Your HTML content'
});
Copied!
DocumentFragment:
document.createRange().createContextualFragment("<div>Your HTML content</div>");
Copied!
severity
severity
Type
SeverityEnum
Required
true
Set severity color for sheet. Color will only affect title bar and
prev- and next-buttons.
progressBarTitle
progressBarTitle
Type
string
Required
true
Set a title for the progress bar. The progress bar will only be shown
below the content section of the slide, if you have defined at least
two or more slides.
callback
callback
Type
SlideCallback
Required
true
A JavaScript callback function which will be called after the slide was
rendered completely.
Show / Hide Wizard
After defining some slides you can show
MultiStepWizard.show() and hide
MultiStepWizard.dismiss() the multi-step wizard.
Lock/Unlock steps
Switching to the next or previous slides is called a step. The buttons
to navigate to the slides are deactivated by default. Please use following
methods to lock or unlock them:
This JavaScript snippet will create a new multi-step wizard with just one
sheet. As it used
SeverityEnum.warning the title and buttons
will be colored in yellow.
import {SeverityEnum} from"@typo3/backend/enum/severity.js"import MultiStepWizard from"@typo3/backend/multi-step-wizard.js"import $ from"jquery";
exportdefaultclassHelloWorldModule{
constructor(triggerHelloWorldWizardButtonClass) {
const buttons = document.querySelectorAll("." + triggerHelloWorldWizardButtonClass);
buttons.forEach((button) => {
button.addEventListener("click", () => {
MultiStepWizard.addSlide(
"UniqueHelloWorldIdentifier",
"Title of the Hello World example slide",
document.createRange().createContextualFragment("<div>Hello world</div>"),
SeverityEnum.warning,
"Step Hello World",
function ($slide) {
let $modal = $slide.closest(".modal");
let $nextButton = $modal.find(".modal-footer").find("button[name='next']");
MultiStepWizard.unlockNextStep();
$nextButton.off().on("click", function () {
// Process whatever you want from current slide, just before wizard will be closed or next slide// Close wizard
MultiStepWizard.dismiss();
// Go to next slide, if any// MultiStepWizard.setup.$carousel.carousel("next");
});
}
);
MultiStepWizard.show();
});
});
}
}
Copied!
To call the JavaScript from above you have to use the
JavaScriptModuleInstruction) technique.
In following snippet you see how to add a JavaScript module to field within
Form Engine:
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:
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.
The module is called
@typo3/backend/storage/abstract-client-storage,
implemented by:
@typo3/backend/storage/browser-session
@typo3/backend/storage/client
get(key)
Fetches the data behind the key.
getByPrefix('common-prefix-')
Obtains multiple items prefixed by a given key.
set(key, value)
Sets/overrides a key with any arbitrary content.
isset(key) (bool)
Checks if the key is in use.
unset(key)
Removes a key from the storage.
clear()
Empties all data inside the storage.
unsetByPrefix(prefix)
Empties all data inside the storage with their keys
starting with a prefix.
Ajax request
TYPO3 Core ships an API to send Ajax requests to the server. This API is based
on the fetch API, which is implemented in every modern browser (for
example, Chrome, Edge, Firefox, Safari).
Prepare a request
New in version 13.0
Native URL-related objects (
URL and
URLSearchParams) can be used.
To be able to send a request, the module
@typo3/core/ajax/ajax-request.js
must be imported. To prepare a request, create a new instance of
AjaxRequest per request and pass the URL as the constructor argument:
import AjaxRequest from"@typo3/core/ajax/ajax-request.js";
let url = 'https://example.org/my-endpoint';
// or:let url = new URL('https://example.org/my-endpoint');
let request = new AjaxRequest(url);
Copied!
The API offers a method
withQueryArguments() which allows to attach a query
string to the URL. This comes in handy, if the query string is programmatically
generated. The method returns a clone of the
AjaxRequest object. It is
possible to pass either strings, arrays or objects as an argument.
The API presets the request configuration with
{credentials: 'same-origin', signal: AbortController.signal}.
The body of the request is automatically converted to a FormData object, if
the submitted payload is an object. To send a JSON-encoded object instead, set
the Content-Type header to application/json. If the payload is a string, no
conversion will happen, but it is still recommended to set proper headers.
response is an object of type
AjaxResponse shipped by TYPO3
(
@typo3/core/ajax/ajax-response.js). The object is a simple wrapper for
the original Response object.
AjaxResponse exposes the following
methods which eases the handling of responses:
resolve()
Returns the correct response based on the received Content-Type header,
either plaintext or a JSON object.
Of course, a request may fail for various reasons. In such case, a second
function may be passed to
then(), which handles the exceptional case. The
function may receive a
AjaxResponse object which contains the original
response object.
import AjaxRequest from"@typo3/core/ajax/ajax-request.js";
let request = new AjaxRequest('https://example.org/my-endpoint');
const json = {foo: 'bar'};
let promise = request.post(json, {
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
promise.then(asyncfunction (response) {
}, function (error) {
console.error(`The request failed with ${error.response.status}: ${error.response.statusText}`);
});
Copied!
Hint
The fetch API handles responses with faulty statuses like 404 or 500 still
as "successful", but sets the response's
ok field to
false. The
Ajax API converts such responses into errors for convenience reasons.
Abort a request
In some cases it might be necessary to abort a running request. The Ajax API has
you covered them, an instance of AbortController is attached to each request.
To abort the request, just call the
abort() method:
request.abort();
Copied!
Event API
The TYPO3 JavaScript Event API enables JavaScript developers to have a stable event listening
interface. The API takes care of common pitfalls like event delegation and clean
event unbinding.
Warning
When using
import statements, it is vital that you use the suffix
.js to any import statements, when you are in the scope of JavaScript/ES6.
Only when you create your code in TypeScript (using .ts suffix), you
need to omit the .js extension in
import statements.
See https://github.com/microsoft/TypeScript/issues/16577 for the reasoning of this.
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.
import RegularEvent from'@typo3/core/event/regular-event.js';
new RegularEvent('click', function (e) {
// Do something
}).bindTo(document.querySelector('#my-element'));
Copied!
Event Delegation
The event listener is called if the event was triggered to any matching element
inside its bound element.
import RegularEvent from'@typo3/core/event/regular-event.js';
new RegularEvent('click', function (e) {
// Do something
}).delegateTo(document, 'a[data-action="toggle"]');
Copied!
The event listener is now called every time the element matching the selector
a[data-action="toggle"] within
document is clicked.
Release an event
Since each event is an object instance, it is sufficient to call
release() to
detach the event listener.
import RegularEvent from'@typo3/core/event/regular-event.js';
const clickEvent = new RegularEvent('click', function (e) {
// Do something
}).delegateTo(document, 'a[data-action="toggle"]');
// Do more stuff
clickEvent.release();
Copied!
Event strategies
The Event API brings several strategies to handle event listeners:
RegularEvent
The
RegularEvent attaches a simple event listener to an event and element
and has no further tweaks. This is the common use case for event handling.
import RegularEvent from'@typo3/core/event/regular-event.js';
new RegularEvent('click', function (e) {
e.preventDefault();
window.location.reload();
}).bindTo(document.querySelector('#my-element'));
Copied!
DebounceEvent
The
DebounceEvent is most suitable if an event is triggered quite often
but executing the event listener is called only after a certain wait time.
Arguments:
eventName (string) - the event to listen on
callback (function) - the event listener
wait (number) - the amount of milliseconds to wait before the event listener is called
Changed in version 13.0
The parameter
immediate has been removed. There is no direct migration
possible. An extension author may re-implement the removed behavior manually,
or use the ThrottleEvent module,
providing a similar behavior.
import DebounceEvent from'@typo3/core/event/debounce-event.js';
new DebounceEvent('mousewheel', function (e) {
console.log('Triggered once after 250ms!');
}, 250).bindTo(document);
Copied!
ThrottleEvent
Arguments:
eventName (string) - the event to listen on
callback (function) - the event listener
limit (number) - the amount of milliseconds to wait before the event listener is called
The
ThrottleEvent is similar to the
DebounceEvent. The important
difference is that the event listener is called after the configured wait time
during the overall event time.
If an event time is about 2000ms and the wait time is configured to be 100ms,
the event listener gets called up to 20 times in total (2000 / 100).
import ThrottleEvent from'@typo3/core/event/throttle-event.js';
new ThrottleEvent('mousewheel', function (e) {
console.log('Triggered every 100ms!');
}, 100).bindTo(document);
Copied!
RequestAnimationFrameEvent
The
RequestAnimationFrameEvent binds its execution to the browser's
RequestAnimationFrame API. It is suitable for event listeners that
manipulate the DOM.
import RequestAnimationFrameEvent from'@typo3/core/event/request-animation-frame-event.js';
new RequestAnimationFrameEvent('mousewheel', function (e) {
console.log('Triggered every 16ms (= 60 FPS)!');
});
Copied!
Hotkey API
New in version 13.0
TYPO3 provides the
@typo3/backend/hotkeys.js module that allows developers
to register custom keyboard shortcuts in the TYPO3 backend.
It is also possible and highly recommended to register hotkeys in a dedicated
scope to avoid conflicts with other hotkeys, perhaps registered by other
extensions.
The module provides an enum with common modifier keys: Ctrl, Meta,
Alt, and Shift), and also a public property describing the common
hotkey modifier based on the user's operating system: Cmd (Meta) on macOS,
Ctrl on anything else (this can be normalized via
Hotkeys.normalizedCtrlModifierKey. Using any modifier is optional, but highly
recommended.
Hint
Note that on macOS, using the
ModifierKeys.ALT to query a
pressed key needs you to listen on the key that results in using this
modifier. So, if you listen on:
instead. To make this work across different operating systems,
it would be recommended to listen on both variants with distinct
javascript callbacks executing the same action. Or, try avoiding
to bind to ModifierKeys.ALT altogether.
A hotkey is registered with the
register() method. The method takes three
arguments:
hotkey
An array defining the keys that must be pressed.
handler
A callback that is executed when the hotkey is invoked.
options
An object that configures a hotkey's behavior:
scope
The scope a hotkey is registered in.
Note
TYPO3-specific hotkeys may be registered in the reserved
all
scope. When invoking a hotkey from a different scope, the
all
scope is handled in any case at first.
allowOnEditables
If
false (default), handlers are not executed when an editable
element is focussed.
allowRepeat
If
false (default), handlers are not executed when the hotkey is
pressed for a long time.
bindElement
If given, an
aria-keyshortcuts attribute is added to the
element. This is recommended for accessibility reasons.
import Hotkeys, {ModifierKeys} from'@typo3/backend/hotkeys.js';
Hotkeys.register([Hotkeys.normalizedCtrlModifierKey, ModifierKeys.SHIFT, 'e'], keyboardEvent => {
console.log('Triggered on Ctrl/Cmd+Shift+E');
}, {
scope: 'my-extension/module',
bindElement: document.querySelector('.some-element')
});
// Get the currently active scopeconst currentScope = Hotkeys.getScope();
// Make use of registered scope
Hotkeys.setScope('my-extension/module');
Copied!
Navigation via JavaScript
Navigate to URL
Navigate to a URL once selected drop-down is changed:
$data refers to value of
data-navigate-value, $value to selected value,
$data=~s/$value/ replaces the literal ${value} with the selected value in
data-navigate-value.
Show info popup
Invoke the
TYPO3.InfoWindow.showItem module function to display details
for a given record:
<aclass="btn btn-default"href="#"data-dispatch-action="TYPO3.InfoWindow.showItem"data-dispatch-args-list="be_users,123"
>
Some text
</a>
Copied!
or (using JSON arguments)
<aclass="btn btn-default"href="#"data-dispatch-action="TYPO3.InfoWindow.showItem"data-dispatch-args="["tt_content",123]"
>
Some text
</a>
Copied!
Shows the info popup of database table
tt_content, having uid=123 in
the example above.
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
<inputtype="checkbox"data-global-event="change"data-action-submit="$form"><!-- ... or (using CSS selector) ... --><inputtype="checkbox"data-global-event="change"data-action-submit="#formIdentifier">
Copied!
Submits a form once a value has been changed.
($form refers to the parent form element, using CSS selectors like #formIdentifier
is possible as well)
Ajax in the backend
An Ajax endpoint in the TYPO3 backend is usually implemented as a method in a
regular controller. The method receives a request object implementing the
\Psr\Http\Message\ServerRequestInterface , which allows to access all
aspects of the requests and returns an appropriate response in a normalized way.
This approach is standardized as PSR-7.
Create a controller
By convention, a controller is placed within the extension's Controller/
directory, optionally in a subdirectory. To have such controller, create a new
ExampleController in Classes/Controller/ExampleController.php
inside your extension.
The controller needs not that much logic right now. We create a method called
doSomethingAction() which will be our Ajax endpoint.
In its current state, the method does nothing yet. We can add a very generic
handling that exponentiates an incoming number by 2. The incoming value will be
passed as a query string argument named input.
The endpoint must be registered as route. Create a
file called Configuration/Backend/AjaxRoutes.php in your extension. The
file basically just returns an array of route definitions. Every route in this
file will be exposed to JavaScript automatically. Let us register our endpoint
now:
The naming of the key myextension_example_dosomething and path
/my-extension/example/do-something are up to you, but should contain the
extension name, controller name and action name to avoid potential conflicts
with other existing routes.
Attention
Flushing caches is mandatory after modifying any route definition.
Protect the endpoint
Important
AJAX routes are accessible to all authenticated backend users by default
and need proper permission checks in order to avoid unauthorized access.
Make sure to protect your endpoint against unauthorized access, if it performs
actions which are limited to authorized backend users only.
Inherit access from backend module
New in version 12.4.37 / 13.4.18
This functionality was introduced in response to security advisory
TYPO3-CORE-SA-2025-021
to mitigate broken access control in backend AJAX routes.
If your endpoint is part of a backend module, you can
configure your endpoint to inherit access rights from this specific module by
using the configuration option inheritAccessFromModule:
In case you're providing a standalone endpoint (that is, the endpoint is not
bound to a specific backend module), make sure to perform proper permission
checks on your own. You can use the
backend user object to perform various
authorization and permission checks on incoming requests.
Use in Ajax
Since the route is registered in AjaxRoutes.php it is exposed to
JavaScript now and stored in the global
TYPO3.settings.ajaxUrls object
identified by the used key in the registration. In this example it is
TYPO3.settings.ajaxUrls.myextension_example_dosomething.
Now you are free to use the endpoint in any of your Ajax calls. To complete this
example, we will ask the server to compute our input and write the result into
the console.
Backend layouts can be defined as database records or via page TSconfig.
Page TSconfig should be preferred as it can be stored in the file system and
be kept under version control.
Backend layout video
Benjamin Kott: How to implement frontend layouts in TYPO3 using backend layouts
Backend layout configuration
The backend layout to be used can be configurated for each page and/or a pages'
subpages in the Page properties > Appearance. Multiple backend
layouts are available if an
extension providing backend layouts is installed or
backend layouts have been
defined as records or page TSconfig.
Choose the backend layout in the page properties
The Info module gives an overview of the backend layouts configured or
inherited from a parent page at
Web > Info > Pagetree overview > Type: Layouts:
Overview of the backend layouts used
Backend layout definition
Backend layouts can be configured either as "backend layout" record in a sysfolder or as page TSconfig entry in
mod.web_layout.BackendLayouts. Each layout will be saved with a key. The "backend layout" records are
using their uid as a key, therefore layouts defined via page TSconfig should use a non-numeric string key. It is a good
practice to use a descriptive name as key.
The entries title and icon are being used to display the backend layout options in the page properties.
The overall grid size will be defined by
config.backend_layout.colCount and
rowCount.
Additional rows in the
rows array and additional columns in the each rows
columns section
will be ignored when they are greater than
rowCount or
colCount respectively.
Each column position can span several columns and or several rows. Each column position must have a distinct number
between 0 and n. It is best practice to always assign "0" to the main column if there is such a thing as a
main column. Multiple backend layouts that contain similar parts, i.e. header, footer, aside, ... should each have
assigned the same number within one project. This leads to a uniform position of the content, which makes it more clear
for further use.
For usage with the page-content data processor, an identifier string must
be assigned to each column. The default backend layout definition uses identifier = main for column 0.
Backend layout simple example
The following page TSconfig example creates a simple backend layout consisting of two rows and just one column.
The following page TSconfig example creates a 3x3 backend layout with 5 column position sections in total. The topmost
row (here called "header") spans all 3 columns. There is an "aside" spanning two rows on the right.
mod.web_layout.BackendLayouts {
exampleKey {
title = Example
icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif
config {
backend_layout {
colCount = 3
rowCount = 3
rows {
1 {
columns {
1 {
identifier = header
name = Header
colspan = 3
colPos = 1
}
}
}
2 {
columns {
1 {
identifier = main
name = Main
colspan = 2
colPos = 0
}
2 {
identifier = aside
name = Aside
rowspan = 2
colPos = 2
}
}
}
3 {
columns {
1 {
identifier = left
name = Main Left
colPos = 5
}
2 {
identifier = right
name = Main Right
colPos = 6
}
}
}
}
}
}
}
}
Copied!
Output of a backend layout in the frontend
The backend layout to be used on a certain page gets determined either by the backend layout being chosen directly and
stored in the pages field "backend_layout" or by the field "backend_layout_next_level" of a parent page up the rootline.
To avoid complex TypoScript for integrators, the handling of backend layouts has
been simplified for the frontend.
To get the correct backend layout, the following TypoScript code can be used:
In many cases besides defining fixed backend layouts a more modular approach with the possibility of combining different
backend layouts and frontend layouts may be feasible. The extension
b13/container
integrates the grid layout concept also to regular content elements.
The extension
ichhabrecht/content-defender
offers advanced options to
the column positions i.e. allowed or disallowed content elements, a maximal number of content elements.
Backend routing
Each request to the backend is eventually executed by a controller.
A list of routes is defined which maps a given request to a controller
and an action.
Routes are defined inside extensions, in the files
<?phpuseTYPO3\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.
Note
The current route object is available as a route attribute in the PSR-7 request object of every
backend request. It is added through the PSR-15 middleware stack and can be
retrieved using
$request->getAttribute('route').
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.
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:
HTTP request to https://example.org/typo3/ having a valid user
session
Internally public backend route /login is processed
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
Attention
Please keep in mind these steps are part of a mitigation strategy, which
requires to be aware of mentioned implications when implementing custom web
applications.
Dynamic URL parts in backend URLs
New in version 12.1
Backend routes can be registered with path segments that contain dynamic parts,
which are then resolved into a PSR-7 request attribute called
routing.
These routes are defined within the route path as named placeholders:
Using the UriBuilder API, you can generate any kind of URL for the backend, may
it be a module, a typical route or an Ajax call. Therefore use either
buildUriFromRoute() or
buildUriFromRoutePath(). The
UriBuilder then returns a PSR-7 conform
Uri object that can be
cast to a string when needed. Furthermore, the
UriBuilder automatically
generates and applies the mentioned session token.
To generate a backend URL via the
UriBuilder you'd usually use the route
identifier and optional
parameters.
In case of Extbase controllers you can append the controller action to the route
identifier to directly target those actions. See also module configuration: controllerActions.
Via Fluid ViewHelper
To generate a backend URL in Fluid you can simply use html:<f:be.link> (which
is using
UriBuilder internally).
<f:be.linkroute="web_layout"parameters="{id:42}">go to page 42</f:be.link><f:be.linkroute="web_ExtkeyExample">go to custom BE module</f:be.link><f:be.linkroute="web_ExtkeyExample.MyModuleController_list">
go to custom BE module but specific controller action
</f:be.link>
Copied!
Via PHP
Example within a controller (we use here a non-Extbase controller):
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Controller;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useTYPO3\CMS\Backend\Routing\UriBuilder;
finalclassMyRouteController{
publicfunction__construct(
private readonly UriBuilder $uriBuilder,
){}
publicfunctionhandle(ServerRequestInterface $request): ResponseInterface{
// ... do some stuff// Using a route identifier
$uri = $this->uriBuilder->buildUriFromRoute(
'web_layout',
['id' => 42],
);
// Using a route path
$uri = $this->uriBuilder->buildUriFromRoutePath(
'/record/edit',
[
'edit' => [
'pages' => [
123 => 'edit',
],
],
],
);
// Using a PSR-7 request object// This is typically useful when linking to the current route or module// in the TYPO3 backend to avoid internals with any PSR-7 request attribute.
$uri = $this->uriBuilder->buildUriFromRequest($request, ['id' => 42]);
// ... do some other stuff
}
}
Copied!
New in version 13.0
The
UriBuilder->buildUriFromRequest() method has been introduced.
Sudo mode
The sudo mode, as known from the install tool,
can be request for arbitrary backend modules.
You can configure the sudo mode in your backend routing like this:
The backend user of a session is always available in extensions
as the global variable
$GLOBALS['BE_USER']. The object is created in
\TYPO3\CMS\Backend\Middleware\BackendUserAuthenticator middleware for a standard web request
and is an instance of the class
\TYPO3\CMS\Core\Authentication\BackendUserAuthentication
(which extends
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication ).
When working with CLI and commands you might initialize the backend user object with
\TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser(). In addition, you can call
\TYPO3\CMS\Core\Core\Bootstrap::initializeBackendAuthentication() to load the language of the CLI user set in the backend so that view helpers (like
f:translate()) used in the CLI resolve to the correct language.
Checking user access
The
$GLOBALS['BE_USER'] object is mostly used to check user access right,
but contains other helpful information. This is presented here by a few examples:
Checking access to any backend module
If you know the module key you can check if the module is included in
the access list by this function call:
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):
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:
Again the number "1" represents the "read" permission; "2" is "edit"
and "4" is "delete" permission. The result from the above query could be this string:
Result of the above query
((pages.perms_everybody & 1 = 1)OR(pages.perms_userid = 2 AND pages.perms_user & 1 = 1)OR(pages.perms_groupid in (1) AND pages.perms_group & 1 = 1))
Copied!
Saving module data
This stores the input variable
$compareFlags (an array!, retrieved from
the request object) with the key
"tools_beuser/index.php/compare":
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:
It is possible to send broadcast messages from anywhere in TYPO3 that are
listened to via JavaScript.
Warning
This API is considered internal and may change anytime until declared being stable.
Send a message
Changed in version 12.0
The RequireJS module
TYPO3/CMS/Backend/BroadcastService has been migrated
to the ES6 module
@typo3/backend/broadcast-service.js.
See also ES6 in the TYPO3 Backend.
Any backend module may send a message using the
@typo3/backend/broadcast-service.jsES6 module.
The payload of such a message is an object that consists at least of the
following properties:
componentName - the name of the component that sends the message (e.g. extension name)
eventName - the event name used to identify the message
A message may contain any other property as necessary. The final event
name to listen is a composition of "typo3", the component name and the
event name, e.g. typo3:my_extension:my_event.
To send a message, the
post() method must be used.
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 detailexcluding the component name and event name.
classMyEventHandler{
constructor() {
document.addEventListener('typo3:my_component:my_event', (e) => eventHandler(e.detail));
}
functioneventHandler(detail) {
console.log(detail); // contains 'hello' and 'foo' as sent in the payload
}
}
exportdefaultnew MyEventHandler();
Hook into
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess']
to load a custom
\TYPO3\CMS\Backend\Controller\BackendController hook
that loads the event handler's JavaScript.
The backend module menu button bar can display
dropdowns. This enables interface interactions, such as switching the current
view from list to tiles, or group actions like clipboard and thumbnail
visibility. This helps unclutter the views and allow the user to see more
information at a glance.
Each dropdown consists of various elements ranging from headings to item links
that can display the current status. The button automatically changes the
icon representation to the icon of the the first active radio icon in the
dropdown list.
DropDown button component in the File > Filelist module
DropDownButton
This button type is a container for dropdown items. It will render a dropdown
containing all items attached to it. There are different kinds available, each
item needs to implement the
\TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownItemInterface .
When this type contains elements of type DropDownRadio
it will use the icon of the first active item of this type.
This dropdown item type renders an element with an active state. Use this
element to display a radio-like selection of a state. When set to active, it
will show a dot in front of the icon and text to indicate that this is the
current selection.
At least two of these items need to exist within a dropdown button, so a user
has a choice of a state to select.
This dropdown item type renders an element with an active state. When set to
active, it will show a checkmark in front of the icon and text to indicate the
current state.
The class
\TYPO3\CMS\Backend\Clipboard\Clipboard is marked
@internal. It is a specific Backend implementation and is not
considered part of the Public TYPO3 API. It might change without notice.
You can easily access the internal clipboard in TYPO3 from your
backend modules:
useTYPO3\CMS\Backend\Clipboard\Clipboard;
useTYPO3\CMS\Core\Utility\DebugUtility;
useTYPO3\CMS\Core\Utility\GeneralUtility;
classModuleControllerextendsActionControllerimplementsLoggerAwareInterface{
protectedfunctiondebugClipboard(){
/** @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:
useTYPO3\CMS\Backend\Clipboard\Clipboard;
useTYPO3\CMS\Core\Utility\GeneralUtility;
classModuleControllerextendsActionControllerimplementsLoggerAwareInterface{
protectedfunctiongetCurrentClipboard():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 shown after clicking on the Content Element icon
Context menu rendering flow
Markup
Changed in version 13.0
The configuration of the context menu was streamlined. Replace
class="t3js-contextmenutrigger" with
data-contextmenu-trigger="click"
data-table="pages" with
data-contextmenu-table="pages"
data-uid="10" with
data-contextmenu-uid="10"
data-context="tree" with
data-contextmenu-context="tree"
to be compatible with TYPO3 v12+.
New in version 12.1
The context menu JavaScript API was adapted to also support opening
the menu through the "contextmenu" event type (right click) only.
The context menu is shown after clicking on the HTML element which has the
data-contextmenu-trigger attribute set together with
data-contextmenu-table,
data-contextmenu-uid and optional
data-contextmenu-context attributes.
The HTML attribute
data-contextmenu-trigger has the following options:
click: Opens the context menu on the "click" and "contextmenu"
events
contextmenu: Opens the context menu only on the "contextmenu" event
The JavaScript click event handler is implemented in the
ES6 module
@typo3/backend/context-menu.js. It takes the
data attributes mentioned above and executes an Ajax call to the
\TYPO3\CMS\Backend\Controller\ContextMenuController->getContextMenuAction().
Changed in version 12.0
The RequireJS module
TYPO3/CMS/Backend/ContextMenu has been migrated
to the ES6 module
@typo3/backend/context-menu.js.
See also ES6 in the TYPO3 Backend.
ContextMenuController
ContextMenuController asks
\TYPO3\CMS\Backend\ContextMenu\ContextMenu
to generate an array of items.
ContextMenu builds a list of available
item providers by asking each whether it can provide items
(
$provider->canHandle()), and what priority it has
(
$provider->getPriority()).
Item providers registration
Changed in version 12.0
ContextMenu item providers, implementing
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface
are now automatically registered. The registration via
$GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders']
is not evaluated anymore.
Custom item providers must implement
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can
extend
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider .
They are then automatically registered by adding the
backend.contextmenu.itemprovider
tag, if
autoconfigure is enabled in Services.yaml. The class
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ItemProvidersRegistry then
automatically receives those services and registers them.
If
autoconfigure is
not enabled in your Configuration/Services.(yaml|php) file,
manually configure your item providers with the
backend.contextmenu.itemprovider tag.
There are two item providers which are always available:
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.
Menu rendering in JavaScript
Changed in version 12.0
The RequireJS module
TYPO3/CMS/Backend/ContextMenuActions has been migrated
to the ES6 module
@typo3/backend/context-menu-actions.js.
See also ES6 in the TYPO3 Backend.
Based on the JSON data context-menu.js is rendering a context menu. If
one of the items is clicked, the according JavaScript
callbackAction is
executed on the ES6 module
@typo3/backend/context-menu-actions.js
or other modules defined in the JSON as
additionalAttributes['data-callback-module'].
Several TYPO3 Core modules are already using this API for adding or modifying
items. See following places for a reference:
EXT:impexp module adds import and export options for pages, content elements
and other records. See item provider
\TYPO3\CMS\Impexp\ContextMenu\ItemProvider and ES6 module
@typo3/impexp/context-menu-actions.js.
EXT:filelist module provides several item providers for files, folders,
file mounts, filestorage, and drag-drop context menu for the folder tree.
See following item providers:
\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileDragProvider,
\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider ,
\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileStorageProvider,
\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FilemountsProvider
and the ES6 module
@typo3/filelist/context-menu-actions.js
Adding context menu to elements in your backend module
Enabling context menu in your own backend modules is quite straightforward.
The examples below are taken from the "beuser" system extension and
assume that the module is Extbase-based.
The first step is to include the needed JavaScript using the
includeJavaScriptModules property
of the standard backend container Fluid view helper (or backend page
renderer view helper).
Doing so in your layout is sufficient (see
typo3/sysext/beuser/Resources/Private/Layouts/Default.html).
<!-- TYPO3 v12 and above --><f:be.pageRendererincludeJavaScriptModules="{0: '@typo3/backend/context-menu.js'}">
// ...
</f:be.pageRenderer><!-- TYPO3 v11 and v12 --><f:be.pageRendererincludeRequireJsModules="{0:'TYPO3/CMS/Backend/ContextMenu'}">
// ...
</f:be.pageRenderer>
Copied!
The second step is to activate the context menu on the icons. This kind of markup
is required (taken from
typo3/sysext/beuser/Resources/Private/Templates/BackendUser/Index.html):
the relevant line being highlighted. The attribute
data-contextmenu-trigger
triggers a context menu functionality for the current element. The
data-contextmenu-table attribute contains a table name of the record and
data-contextmenu-uid the
uid of the record.
The attribute
data-contextmenu-trigger has the following options:
click: Opens the context menu on the "click" and "contextmenu"
events
contextmenu: Opens the context menu only on the "contextmenu" event
One additional data attribute can be used
data-contextmenu-context with
values being, for example,
tree for context menu triggered from
the page tree. Context is used to hide menu items independently for page tree
independently from other places (disabled items can be configured in TSconfig).
Note
In most cases the
data-contextmenu-uid attribute contains an integer value.
However, in case of files and folders this attribute takes file/folder path
as a value like
data-contextmenu-uid="1:/some-folder/some-file.pdf"
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:
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.
Context menu with custom item
Step 1: Implementation of the item provider class
Implement your own item provider class. Provider must implement
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and
can extend
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider
or any other provider from EXT:backend.
See comments in the following code snippet clarifying implementation details.
<?phpnamespaceT3docs\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!
*/useTYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;
/**
* Item provider adding Hello World item
*/classHelloWorldItemProviderextendsAbstractProvider{
/**
* This array contains configuration for items you want to add
* @var array
*/protected $itemsConfiguration = [
'hello' => [
'type' => 'item',
'label' => 'Hello World', // you can use "LLL:" syntax here'iconIdentifier' => 'actions-lightbulb-on',
'callbackAction' => 'helloWorld', //name of the function in the JS file
],
];
/**
* Checks if this provider may be called to provide the list of context menu items for given table.
*
* @return bool
*/publicfunctioncanHandle(): bool{
// Current table is: $this->table// Current UID is: $this->identifier// return $this->table === 'pages';returntrue;
}
/**
* 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
*/publicfunctiongetPriority(): int{
return55;
}
/**
* 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
*/protectedfunctiongetAdditionalAttributes(string $itemName): array{
return [
'data-callback-module' => '@t3docs/examples/context-menu-actions',
// Here you can also add any other useful "data-" attribute you'd like to use in your JavaScript (e.g. localized messages)
];
}
/**
* This method adds custom item to list of items generated by item providers with higher priority value (PageProvider)
* You could also modify existing items here.
* The new item is added after the 'info' item.
*
* @param array $items
* @return array
*/publicfunctionaddItems(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 providerreturn $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
*/protectedfunctioncanRender(string $itemName, string $type): bool{
// checking if item is disabled through TSConfigif (in_array($itemName, $this->disabledItems, true)) {
returnfalse;
}
$canRender = false;
switch ($itemName) {
case'hello':
$canRender = $this->canSayHello();
break;
}
return $canRender;
}
/**
* Helper method implementing e.g. access check for certain item
*
* @return bool
*/protectedfunctioncanSayHello(): bool{
//usually here you can find more sophisticated condition. See e.g. PageProvider::canBeEdited()returntrue;
}
}
Copied!
Step 2: JavaScript actions
Provide a JavaScript file (ES6 module) which will be
called after clicking on the context menu item.
/**
* Module: @t3docs/examples/context-menu-actions
*
* JavaScript to handle the click action of the "Hello World" context menu item
*/classContextMenuActions{
helloWorld(table, uid) {
if (table === 'pages') {
//If needed, you can access other 'data' attributes here from $(this).data('someKey')//see item provider getAdditionalAttributes method to see how to pass custom data attributes
top.TYPO3.Notification.error('Hello World', 'Hi there!', 5);
}
};
}
exportdefaultnew ContextMenuActions();
Copied!
Register the JavaScript ES6 modules of your extension if not done yet:
If you have
autoconfigure: true set in your extension's Services.yaml file all
classes implementing
\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface
get registered as context menu items automatically:
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
<?phpdeclare(strict_types=1);
defined('TYPO3') ordie();
// Register some custom permission options shown in BE group access lists
$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']['tx_styleguide_custom'] = [
'header' => 'Custom styleguide permissions',
'items' => [
'key1' => [
'Option 1',
// Icon has been registered in Icons.php'tcarecords-tx_styleguide_forms-default',
'Description 1',
],
'key2' => [
'Option 2',
],
],
];
Copied!
The result is that these options appear in the group access lists like
this:
The custom permissions appear in the Access List tab of backend user groups
As you can see it is possible to add both an icon and a description text.
If icons not provided by the Core are used, they need to be registered
with the Icon API.
Evaluation
To check if a custom permission option is set call the following API
function from the user object:
$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.
Attention
Never pick a key containing any of the characters
",:\|". They are reserved delimiter characters.
Backend login form API
Registering a login provider
The concept of the backend login is based on "login providers".
A login provider can be registered within your config/system/settings.php
or config/system/additional.php like this:
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.
Method
LoginProviderInterface->render() has been marked as deprecated
and is substituted by
LoginProviderInterface->modifyView() that will
be added to the interface in TYPO3 v14, removing
render() from the
interface in v14. See section Migration.
The
LoginProviderInterface contains
only the deprecated render() method in TYPO3 v13.
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.
Consumers of
LoginProviderInterface
should implement the
modifyView() method and and retain a stub for the
render() method to satisfy the interface. See the example below.
The transition should be smooth. Consumers that need
\TYPO3\CMS\Core\Page\PageRenderer for JavaScript magic, should use
dependency injection to receive an instance
of it.
An implementation of
LoginProviderInterface could
look like this for TYPO3 v13:
<?phpnamespaceMyVendor\MyExtension\Login;usePsr\Http\Message\ServerRequestInterface;useSymfony\Component\DependencyInjection\Attribute\Autoconfigure;useTYPO3\CMS\Backend\Controller\LoginController;useTYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;useTYPO3\CMS\Core\Page\PageRenderer;useTYPO3\CMS\Core\View\ViewInterface;useTYPO3\CMS\Fluid\View\FluidViewAdapter;useTYPO3\CMS\Fluid\View\StandaloneView;#[Autoconfigure(public: true)]final readonly classMyLoginProviderimplementsLoginProviderInterface
{publicfunction__construct( private PageRenderer $pageRenderer, ){}/** * todo: Remove when dropping TYPO3 v13 support * @deprecated Remove in v14 when method is removed from LoginProviderInterface */publicfunctionrender(StandaloneView $view, PageRenderer $pageRenderer, LoginController $loginController){thrownew \RuntimeException('Legacy interface implementation. Should not be called', 123456789); }publicfunctionmodifyView(ServerRequestInterface $request, ViewInterface $view): string{$this->pageRenderer->addJsFile('someFile');// Custom login provider implementations can add custom fluid lookup paths.if ($view instanceof FluidViewAdapter) { $templatePaths = $view->getRenderingContext()->getTemplatePaths(); $templateRootPaths = $templatePaths->getTemplateRootPaths(); $templateRootPaths[] = 'EXT:my_extension/Resources/Private/Templates'; $templatePaths->setTemplateRootPaths($templateRootPaths); } $view->assign('Some Variable', 'some value');return'Login/MyLoginForm'; }}
Copied!
The default implementation in
UsernamePasswordLoginProvider
is a good example. Extensions that need to configure additional template, layout or
partial lookup paths can extend them, see lines 23-28 in the example above.
Consumers of
ModifyPageLayoutOnLoginProviderSelectionEvent
should use the request instead, and/or should get an instance of
PageRenderer injected as well.
The view
Deprecated since version 13.3
Method
LoginProviderInterface->render() has been marked as deprecated
and is substituted by
LoginProviderInterface->modifyView() that will
be added to the interface in TYPO3 v14, removing
render() from the
interface in v14. See section Migration.
The name of the template must be returned by the modifyView() method of the
login provider. Variables can be assigned to the view supplied as
second parameter.
The template file must only contain the form fields, not the form-tag.
Later on, the view renders the complete login screen.
View requirements:
The template must use the Login-layout provided by the
Core <f:layout name="Login">.
Form fields must be provided within the section
<f:section name="loginFormFields">.
The page tree is a hierarchical structure that represents pages and their
subpages on a website, allowing editors and integrators to easily organize and
manage content and navigation.
It is displayed on the left of backend modules with
navigationComponent
set to '@typo3/backend/tree/page-tree-element'.
Allows to modify the populated properties of a page and children records
before the page is displayed in a page tree.
TsConfig settings to influence the page tree
The rendering of the page tree can be influenced via user TsConfig
options.pageTree.
Use the backend UriBuilder to link to "Edit Records"
It is often needed to create links to edit records in the TYPO3 backend.
The same syntax is also used for creating new records.
TYPO3 provides an API for creating such links, namely
\TYPO3\CMS\Backend\Routing\UriBuilder.
Hint
Make sure to use \TYPO3\CMS\Backend\Routing\UriBuilder to create
backend links and not \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder.
The variable available as $this->uriBuilder in a controller is the
web routing UriBuilder and can only be used for frontend links.
When using Fluid templates the URI
either has to be created via PHP in the controller or a ViewHelper to be used.
The examples above leads to the normal edit form for a page:
Page 1 ready for editing with the standard form
Additional options for editing records
When creating the link via PHP it is possible to add more options.
You can specify as many tables and UIDs as needed and you will get them all in
one single form!
(short way of editing more records from the same table at once).
Also the fields to be displayed can be restricted.
The fields to be included can be listed in the columnsOnly parameter, as a comma-separated list.
The order of the fields doesn't matter, they get displayed in the order they appear in the TCA.
If a field is missing or access restricted in one of the tables it just doesn't appear.
However if one record to be edited is missing none of the records gets displayed.
The example above results in the following:
Editing of fields of two pages and one haiku at once
The link triggers the creation a new record for the table tx_examples_haiku
on page 1. It also sets a default value for the title field ("New haiku") and
selects the season "Spring". It only displays the fields defined by columnsOnly.
Note the following things:
the first parameter is called "edit" even if this is about creating a new record.
The creation of a record is indicated by the value "new".
the key of the entry with value "new" indicates the pid on which the record is to be created.
the values get automatically url-encoded so you can use any special char in the defaults
This results in the following new record form with a pre-filled
title and season field.
Form for creating a new haiku with pre-filled title and season and restricted columns
Migration: Table dependant definition of columnsOnly
The fields to be rendered in a Backend UriBuilder have to be passed as
array under the corresponding table name instead of using a comma
separated string.
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.
Before version 8.1, PHP had no enumeration concept as part of the language.
Therefore the TYPO3 Core includes a custom enumeration implementation.
In TYPO3, enumerations are implemented by extending the abstract class
\TYPO3\CMS\Core\Type\Enumeration . It was originally implemented similar
to
\SplEnum which is unfortunately part of the unmaintained package
PECL spl_types.
With PHP version 8.1, an enumeration concept was implemented (see the
Enumeration documentation for more details). This makes it possible to drop
the custom enumeration concept from the Core in a future TYPO3 version.
How to use enumerations
Changed in version 13.0
The abstract class
\TYPO3\CMS\Core\Type\Enumeration is deprecated.
Classes extending
Enumeration need to be converted into PHP built-in
backed enums.
See Migration to backed enums.
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.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Enumerations;
useTYPO3\CMS\Core\Type\Enumeration;
finalclassLikeWildcardextendsEnumeration{
publicconst __default = self::BOTH;
/**
* @var int Do not use any wildcard
*/publicconst NONE = 0;
/**
* @var int Use wildcard on left side
*/publicconst LEFT = 1;
/**
* @var int Use wildcard on right side
*/publicconst RIGHT = 2;
/**
* @var int Use wildcard on both sides
*/publicconst 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
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension;
useMyVendor\MyExtension\Enumerations\LikeWildcard;
finalclassSomeClass{
publicfunctiondoSomething(){
// ...
$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.
Attention
Always be prepared to handle exceptions when instantiating
enumerations from user defined values!
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:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension;
finalclassSomeClass{
publicfunctiondoSomething(){
// ...
$myVersionState = VersionState::cast($versionStateValue);
if ($myVersionState->indicatesPlaceholder()) {
echo'The state indicates that this is a placeholder';
}
// ...
}
// ...
}
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:
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.
Note
The cache contains placeholders for dynamic elements to tell TYPO3 where to
render dynamic parts.
Hint
For developers: If you are developing a plugin,
think about your plugin's cache lifetime. Ideally, it can be fully cached,
but if not, read the section about the
caching framework to learn how to leverage
TYPO3's caching mechanism to cache your plugin for however long you can -
even 30 seconds might improve performance in some scenarios.
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)
staticRouteArguments: Any route argument configured in the
routing configuration and resolved in the current
request
dynamicArguments: Any "GET" parameters influencing the rendered page
Imagine the following URL https://example.org/news/?tx_example_news[id]=123
displaying the news with ID 123. If TYPO3 would cache that page with that
parameter without any security mechanisms, it would open a potential denial of
service attack as an unlimited amount of cache entries could be generated by
adding arbitrary parameters. To avoid that, TYPO3 generates a so-called "cHash"
parameter, which is a hash that basically signs the valid parameters for that
request. So any parameter that validly influences the rendered page needs to be
part of that cHash.
With routing you can configure TYPO3 not to display the cHash
in your URLs in most cases. Routing adds an explicit mapping of incoming
readable URL slugs to internal parameter values. This both adds an additional
layer for validating slugs as well as reduces the parameters to a limited (and
predictable) set of values.
Various configuration options exist to configure the cHash behavior via
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']
in the file config/system/settings.php or config/system/additional.php:
cachedParametersWhiteList
cachedParametersWhiteList
Only the given parameters will be evaluated in the cHash calculation.
Example: tx_news_pi1[uid]
Attention
This option will lead to cache calculation being skipped for all
parameters except the ones listed here. Caching of pages will not be
influenced by other parameters beyond the initial caching anymore.
requireCacheHashPresenceParameters
requireCacheHashPresenceParameters
Configure parameters that require a cHash. If no cHash is given, but one of
the parameters are set, then TYPO3 triggers the configured cHash error
behavior
excludedParameters
excludedParameters
The given parameters will be ignored in the cHash calculation.
Example: L,tx_search_pi1[query]
excludedParametersIfEmpty
excludedParametersIfEmpty
Configure parameters only being relevant for the cHash if there is an
associated value available. Set excludeAllEmptyParameters to true to skip
all empty parameters.
excludeAllEmptyParameters
excludeAllEmptyParameters
If true, all parameters relevant to cHash are only considered when they are
not empty.
enforceValidation
enforceValidation
If this option is enabled, the same validation is used to calculate a cHash
value as when a valid or invalid "cHash" parameter is given to a request,
even when no cHash is given.
All properties can be configured with an array of values. Besides exact matches
(equals) it is possible to apply partial matches at the beginning of a
parameter (startsWith) or inline occurrences (contains).
URL parameter names are prefixed with the following indicators:
= (equals): exact match, default behavior if not given
^ (startsWith): matching the beginning of a parameter name
~ (contains): matching any inline occurrence in a parameter name
These indicators can be used for all previously existing sub-properties
cachedParametersWhiteList,
excludedParameters,
excludedParametersIfEmpty
and
requireCacheHashPresenceParameters.
Clearing the cache for a single page is done by using the "clear cache button"
on that page in the backend (usually visualized with a "bolt" icon). Depending
on user rights this option is available for editors.
Clearing the frontend caches and all caches can be done via the system toolbar
in the TYPO3 backend. This option should only be available to administrators.
Clearing all caches can have a significant impact on the performance of the web
site and should be used sparingly on production systems.
The "fullest" option to clear all caches can be reached via the
System Tools > Maintenance section. In the development context when
new classes have been added to the system or in case of problems with the system
using this cache clearing option will clear all caches including compiled code
like the dependency injection container.
Clear cache command
In addition to the GUI options explained above, caches can also be cleared
via a CLI command:
vendor/bin/typo3 cache:flush
Copied!
typo3/sysext/core/bin/typo3 cache:flush
Copied!
Specific cache groups can be defined via the group option.
The usage is described as this:
cache:flush [--group <all|system|di|pages|...>]
Copied!
All available cache groups can be supplied as
option. The command defaults to flush all available cache groups as the
System Tools > Maintenance area does.
Extensions that register custom caches may listen to the CacheFlushEvent,
but usually the cache flush via cache manager groups will suffice to clear those
caches, too.
Cache warmup
Changed in version 13.4.19
Cache warm-up now fails if the PHP major or minor version used by the
CLI differs from the version used by the web server.
(Both numbers are part of the cache key.)
It is possible to warmup TYPO3 caches using the command line.
The administrator can use the following
CLI command:
vendor/bin/typo3 cache:warmup
Copied!
typo3/sysext/core/bin/typo3 cache:warmup
Copied!
Important
Cache warming will not work correctly if the PHP version used for the
CLI context differs in the major or minor version from the PHP version used for
the web context.
Specific cache groups can be defined via the group option.
The usage is described as this:
cache:warmup [--group <all|system|di|pages|...>]
Copied!
All available cache groups can be supplied as
option. The command defaults to warm all available cache groups.
Extensions that register custom caches are encouraged to implement cache warmers
via CacheWarmupEvent.
Note
TYPO3 frontend caches will not be warmed by TYPO3 Core, such functionality
could be added by third-party extensions with the help of
CacheWarmupEvent.
Use case: deployment
It is often required to clear caches during deployment of TYPO3 instance.
The integrator may decide to flush all caches or may alternatively flush
selected groups (for example, "pages"). It is common practice to clear all
caches during deployment of a TYPO3 instance update. This means that the first
request after a deployment usually takes a major amount of time and blocks other
requests due to cache-locks.
TYPO3 caches can be warmed during deployment in release preparatory steps in
symlink-based deployment/release procedures. This enables fast first requests
with all (or at least system) caches being prepared and warmed.
Caches are often filesystem relevant (file paths are calculated into cache
hashes), therefore cache warmup should only be performed on the the live system,
in the final folder of a new release, and ideally before switching
to that new release (via symlink switch).
Warning
Caches that have been pre-created on the continuous integration server
will likely be useless as cache hashes will not match (as the final file
system path is relevant to the hash generation).
To summarize: Cache warmup is to be used during deployment, on the live system
server, inside the new release folder and before switching to the new release.
An example deployment could consist of:
Before the release:
git-checkout/rsync your codebase to the continuous integration / build
server
composer install on the continuous integration / build server
vendor/bin/typo3 cache:warmup --group system (only on the live
system)
Change release symlink to the new release folder
After the release:
vendor/bin/typo3 cache:flush --group pages
The conceptional idea is to warmup all file-related caches before (symlink)
switching to a new release and to only flush database and frontend (shared)
caches after the symlink switch. Database warmup could be implemented with
the help of the CacheWarmupEvent as an additionally functionality by
third-party extensions.
Note that file-related caches (summarized into the group "system") can safely be
cleared before doing a release switch, as it is recommended to keep file caches
per release. In other words, share var/session/, var/log/,
var/lock/ and var/charset/ between releases, but keep
var/cache/ be associated only with one release.
Caching framework
TYPO3 contains a data caching framework which supports a wide variety of storage
solutions and options for different caching needs. Each cache can be configured
individually and can implement its own specific storage strategy.
The caching framework exists to help speeding up TYPO3 sites, especially
heavily loaded ones. It is possible to move all caches to a dedicated cache
server with specialized cache systems like the Redis key-value store (a so
called NoSQL database).
Major parts of the original caching framework were originally backported from
TYPO3 Flow.
If specific settings should be applied to the configuration, they should be
added to config/system/settings.php. All settings in
config/system/settings.php will be merged with
DefaultConfiguration.php. The easiest way to see the final cache
configuration is to use the TYPO3 backend module
System > Configuration > $GLOBALS['TYPO3_CONF_VARS']
(with installed lowlevel system extension).
Example for a configuration of a
Redis cache backend on Redis database number 42
instead of the default database backend with compression for the pages cache:
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.
Registry of all configured caches. Each cache is identified by its array
key. Each cache can have the sub-keys
frontend,
backend and
options to configure the used frontend, backend and possible backend
options.
Cache configurations
Unfortunately in TYPO3, all ext_localconf.php files of the extensions
are loaded after the instance-specific configuration from
config/system/settings.php and config/system/additional.php.
This enables extensions to overwrite cache configurations already done for the
instance. All extensions should avoid this situation and should define the very
bare minimum of cache configurations. This boils down to define the array key to
populate a new cache to the system. Without further configuration, the cache
system falls back to the default backend and default frontend settings:
Extensions, like Extbase, define default caches this way,
giving administrators full freedom for specific and possibly quicker setups
(for example, a memory-driven cache for the Extbase reflection cache).
Administrators can overwrite specific settings of the cache configuration in
config/system/settings.php or config/system/additional.php. Here
is an example configuration to switch pages to the Redis backend using
database 3:
Some backends have mandatory as well as optional parameters (which are
documented in the Cache backends section). If not all
mandatory options are defined, the specific backend will throw an exception, if
accessed.
How to disable specific caches
During development it can be convenient to disable certain caches. This is
especially helpful for central caches like the language or autoloader cache.
This can be achieved by using the null backend as
storage backend.
Warning
Do not use this in production, it will slow the system down considerably!
Example configuration to switch the extbase_reflection cache to use the
null backend:
The caching framework can handle multiple caches with different configurations.
A single cache consists of any number of cache entries.
A single cache entry is defined by these fields:
identifier
A string as unique identifier within this cache. It is used to store and
retrieve entries.
data
The data to be cached.
lifetime
A lifetime in seconds of this cache entry. An entry can not be retrieved
from cache, if the lifetime is expired.
tags
Additional tags (an array of strings) assigned to the entry. It is used to
remove specific cache entries.
Tip
The difference between identifier and tags is quite simple: an identifier
uniquely identifies a cache entry, and a tag is additional data applied to
an entry (used for cache eviction). Thus, an identifier refers to a single
cache entry to store and retrieve an entry, and a tag can refer to multiple
cache entries.
About the identifier
The identifier is used to store ("set") and retrieve ("get") entries from the
cache and holds all information to differentiate entries from each other.
For performance reasons, it should be quick to calculate.
Suppose a resource-intensive extension is added as a plugin on two different
pages. The calculated content depends on the page on which it is inserted and
if a user is logged in or not. So, the plugin creates at maximum four different
content outputs, which can be cached in four different cache entries:
page 1, no user logged in
page 1, a user is logged in
page 2, no user logged in
page 2, a user is logged in
To differentiate all entries from each other, the identifier is built from the
page ID where the plugin is located, combined with the information whether a
user is logged in. These are concatenated and hashed. In PHP this could look
like this:
sha1 is a good hash algorithm in this case, as collisions are extremely
unlikely. It scales O(n) with the input length.
When the plugin is accessed, the identifier is calculated early in the program
flow. Next, the plugin looks up for a cache entry with this identifier.
If such an entry exists, the plugin can return the cached content,
else it calculates the content and stores a new cache entry with this identifier.
In general, the identifier is constructed from all dependencies
which specify a unique set of data. The identifier should be based on
information which already exist in the system at the point of its calculation.
In the above scenario the page ID and whether or not a user is logged in
are already determined during the frontend bootstrap and
can be retrieved from the system quickly.
About tags
Tags are used to drop specific cache entries when some information they are
based on is changed.
Suppose the above plugin displays content based on different news entries.
If one news entry is changed in the backend, all cache entries
which are compiled from this news row must be dropped to ensure that
the frontend renders the plugin content again and does not deliver old content
on the next frontend call.
For example, if the plugin uses news number one and two on one page, and news
one on another page, the related cache entries should be tagged with these tags:
page 1, tags news_1, news_2
page 2, tag news_1
If entry 2 is changed, a simple backend logic (probably a hook in
DataHandler) could be created, which drops all cache
entries tagged with news_2. In this case the first entry would be
invalidated while the second entry still exists in the cache after the
operation.
While there is always exactly one identifier for each cache entry,
an arbitrary number of tags can be assigned to an entry and one specific tag
can be assigned to multiple cache entries. All tags a cache entry has are given
to the cache when the entry is stored ("set").
The TYPO3 Core defines and uses several caching framework caches by default.
This section gives an overview of default caches, its usage and behaviour. If
not stated otherwise, the default database backend
with variable frontend is used.
The various caches are organized in groups. Currently, the following groups
exist:
pages
Frontend-related caches.
system
System caches. Flushing system caches should be avoided as much
as possible, as rebuilding them requires significant resources.
lowlevel
Low-level caches. Flushing low-level caches manually should be avoided
completely.
all
All other caches.
Cache clearing commands can be issued to target a particular group. If a cache
does not belong to a group, it will be flushed when the "all" group is flushed,
but such caches should normally be transient
anyway.
This cache is instantiated very early during
bootstrap and can not be re-configured
by instance-specific config/system/settings.php or similar.
Cache entries are located in directory var/cache/code/core/
(for Composer-based installations) and
typo3temp/var/cache/code/core/ (for Classic mode installations). The
full directory and any file in this directory can be safely removed and
will be re-created upon next request. This is especially useful during
development
hash
groups: all, pages
Stores several key-value based cache entries, mostly used during
frontend rendering.
pages
groups: all, pages
The frontend page cache. It stores full frontend pages.
The content is compressed by default to reduce database memory and
storage overhead.
runtime
Runtime cache to store data specific for current request.
Used by several Core parts during rendering to re-use already calculated
data.
Valid for one request only.
Can be re-used by extensions that have similar caching needs.
rootline
groups: all, pages
Cache for rootline calculations.
A quick and simple cache dedicated for Core usage, it should not be
re-used by extensions.
assets
groups: system
Cache for assets.
Examples: backend icons, RTE or JavaScript configuration.
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.
Tip
In rare cases, for example, when classes that are required during the
bootstrap process are introduced (usually when working on the TYPO3 Core),
cache clearings requests themselves might throw fatal errors.
The solution here is to manually remove the cache files from
var/cache/code/ (for Composer-based installations) or
typo3temp/var/cache/code/ (for Classic mode installations).
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.
Note
The
\TYPO3\CMS\Core\Cache\CacheManager was used before TYPO3 v10.1 to
retrieve an object implementing
FrontendInterface. It is now
recommended to use dependency injection
to retrieve this object and no longer use the
CacheManager directly.
Warning
Do not use the
CacheManager in ext_localconf.php - instead
load caches on demand at the place where they are needed.
Cache frontends
A cache frontend is the public API for interacting with a cache. It defines
which value types are accepted and how they are prepared for storage
(for example serialization or compilation), while delegating persistence to the
assigned backend. In everyday use, extensions should work with the frontend
only — direct access to the caching backend is discouraged.
Caching frontend API
All caching frontends must implement the interface
\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface .
Something which identifies the data - depends on concrete cache
param $data
The data to cache - also depends on the concrete cache implementation
param $tags
Tags to associate with this cache entry, default: []
param $lifetime
Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL
get(?string $entryIdentifier)
Finds and returns data from the cache.
param $entryIdentifier
Something which identifies the cache entry - depends on concrete cache
Returns
mixed
has(?string $entryIdentifier)
Checks if a cache entry with the specified identifier exists.
param $entryIdentifier
An identifier specifying the cache entry
Return description
TRUE if such an entry exists, FALSE if not
Returns
bool
remove(?string $entryIdentifier)
Removes the given cache entry from the cache.
param $entryIdentifier
An identifier specifying the cache entry
Return description
TRUE if such an entry exists, FALSE if not
Returns
bool
flush()
Removes all cache entries of this cache.
flushByTag(?string $tag)
Removes all cache entries of this cache which are tagged by the specified tag.
param $tag
The tag the entries must have
flushByTags(array $tags)
Removes all cache entries of this cache which are tagged by any of the specified tags.
param $tags
List of tags
collectGarbage()
Does garbage collection
isValidEntryIdentifier(?string $identifier)
Checks the validity of an entry identifier. Returns TRUE if it's valid.
param $identifier
An identifier to be checked for validity
Returns
bool
isValidTag(?string $tag)
Checks the validity of a tag. Returns TRUE if it's valid.
param $tag
A tag to be checked for validity
Returns
bool
The specific cache frontend implementation migth offer additional methods.
Available cache frontend implementations
Two frontends are currently available. They primarily differ in the data
types they accept and how values are handled before they are passed to
the backend.
Variable frontend
This frontend accepts strings, arrays, and objects.
Values are serialized before they are written to the caching backend.
It is implemented in
\TYPO3\CMS\Core\Cache\Frontend\VariableFrontend .
Tip
The variable frontend is the most frequently used frontend and handles
the widest range of data types.
PHP frontend
This frontend is specialized for caching executable PHP files. It adds the
methods
requireOnce() and
require() to load a cached file
directly. This is useful for extensions that generate PHP code at runtime,
for example when heavy reflection or dynamic class construction is involved.
It is implemented in
\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend .
A backend used with the PHP frontend must implement
\TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface . The file
backend and the simple file backend currently fulfill this requirement.
In addition to the methods defined by
\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface , it provides:
requireOnce($entryIdentifier)
Loads PHP code from the cache and
require_once it right away.
require(string $entryIdentifier)
Loads PHP code from the cache and require() it right away.
Unlike require_once(), require() is safe only when the cached code can be
included multiple times within a single request. Files that declare classes,
functions, or constants may trigger redeclaration errors.
Note
The PHP caching frontend can only be used to cache PHP files.
It does not work with strings, arrays or objects.
It is not intended as a page content cache.
Cache backends
There are a variety of different storage backends. They have different characteristics
and can be used for different caching needs. The best backend depends on
your server setup and hardware, as well as cache type and usage.
A backend should be chosen wisely, as the wrong backend can
slow down your TYPO3 installation.
Backend API
All backends must implement the TYPO3\CMS\Core\Cache\Backend\BackendInterface.
Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored., default: []
param $lifetime
Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL
get(?string $entryIdentifier)
Loads data from the cache.
param $entryIdentifier
An identifier which describes the cache entry to load
Return description
The cache entry's content as a string or FALSE if the cache entry could not be loaded
Returns
mixed
has(?string $entryIdentifier)
Checks if a cache entry with the specified identifier exists.
param $entryIdentifier
An identifier specifying the cache entry
Return description
TRUE if such an entry exists, FALSE if not
Returns
bool
remove(?string $entryIdentifier)
Removes all cache entries matching the specified identifier.
Usually this only affects one entry but if - for what reason ever -
old entries for the identifier still exist, they are removed as well.
param $entryIdentifier
Specifies the cache entry to remove
Return description
TRUE if (at least) an entry could be removed or FALSE if no entry was found
Returns
bool
flush()
Removes all cache entries of this cache.
collectGarbage()
Does garbage collection
All operations on caches must use the methods above. There are
other interfaces that can be implemented by backends to add additional functionality.
Extension code should not call cache backend operations
directly, but should use the frontend object instead.
A contract for a cache backend which can be frozen.
freeze()
Freezes this cache backend.
All data in a frozen backend remains unchanged and methods which try to add
or modify data result in an exception thrown. Possible expiry times of
individual cache entries are ignored.
On the positive side, a frozen cache backend is much faster on read access.
A frozen backend can only be thawn by calling the flush() method.
This backend stores data in a database (usually MySQL)
and can handle large amounts of data with reasonable performance.
Data and tags are stored in two different tables and every cache has its own set of tables.
In terms of performance, the database backend is well optimized
and, if in doubt, should be used as a default backend doubt. This is the default
backend if no backend is specifically set in the configuration.
The Core takes care of creating and updating database tables "on the fly".
Note
Caching framework tables which are no longer required are not
automatically deleted. That is why the database analyzer in the Install
Tool will suggest renaming/deleting caching framework tables if you
change the caching backend to a non-database one.
For caches with a lot of read and write operations, it is important to tune your MySQL setup.
The most important setting is innodb_buffer_pool_size. It is a good idea to give MySQL
as much RAM as needed so that the main table space is completely loaded in memory.
The database backend tends to slow down if there are many write operations
and big caches which don't fit into memory because of slow hard drive seek and
write performance. If the data table is too big to fit into memory, this backend can
compress data transparently, which shrinks the amount of
space needed to 1/4 or less. The overhead of the compress/uncompress operation is usually not high.
A good candidate for a cache with enabled compression is the Core pages cache:
it is only read or written once per request and the data size is pretty large.
Compression should not be enabled for caches which are read or written
multiple times in one request.
InnoDB Issues
The MySQL database backend uses InnoDB tables. Due to the nature of InnoDB, deleting records
does not reclaim disk space. For example, if the cache uses 10GB,
cleaning still keeps 10GB allocated on the disk even though phpMyAdmin shows 0 as the cache table size.
To reclaim the space, turn on the MySQL option file_per_table, drop the cache tables and re-create
them using the Install Tool.
This does not mean that you should skip the scheduler task. Deleting records still improves performance.
Options of database backends
compression
compression
Type
boolean
Default
false
Whether or not data should be compressed with gzip.
This can reduce the size of the cache data table, but incurs CPU overhead
for compression and decompression.
compressionLevel
compressionLevel
Type
integer from -1 to 9
Default
-1
Gzip compression level (if the compression option is set to true).
The default compression level is usually sufficient.
-1
Default gzip compression (recommended)
0
No compression
9
Maximum compression (costs a lot of CPU)
Memcached backend
Memcached is a simple, distributed key/value RAM database.
To use this backend, at least one memcached daemon must be reachable,
and the PECL module "memcache" must be loaded.
There are two PHP memcached implementations: "memcache" and "memcached".
Currently, only memcache is supported by this backend.
Limitations of memcached backends
Memcached is a simple key-value store by design . Since the caching framework
needs to structure it to store the identifier-data-tags relations, for each
cache entry it stores an identifier->data, identifier->tags and a
tag->identifiers entry.
This leads to structural problems:
If memcache runs out of memory but must store new entries,
it will toss some other entry out of the cache
(this is called an eviction in memcached speak).
If data is shared over multiple memcache servers and a server fails,
key/value pairs on this system will just vanish from cache.
Both cases lead to corrupted caches. If, for example, a tags->identifier entry is lost,
dropByTag() will not be able to find the corresponding identifier->data entries
to be removed and they will not be deleted. This results in old data being delivered by the cache.
There is currently no implementation of garbage collection that
could rebuild cache integrity.
It is important to monitor a memcached system for evictions and server outages
and to clear caches if that happens.
Furthermore, memcache has no namespacing.
To distinguish entries of multiple caches from each other,
every entry is prefixed with the cache name.
This can lead to very long run times if a big cache needs to be flushed,
as every entry has to be handled separately. It would not be possible
to just truncate the whole cache with one call as this would clear
the whole memcached data which might also contain non-TYPO3-related entries.
Because of the these drawbacks, the memcached backend should be used with care.
It should be used in situations where cache integrity is not important or if a
cache does not need to use tags. Currently, the memcache backend implements the
TaggableBackendInterface, so the implementation does allow tagging,
even if it is not advisable to use this backend with heavy tagging.
Warning
Since memcached does not have namespacing and access control,
this backend should not be used if different third party systems have access
to the same memcached daemon - for security reasons.
This is a typical problem in cloud deployments where access to memcache is cheap
(but could be read by third parties) and access to databases is expensive.
Options for the memcached backend
servers
servers
Type
array
Required
true
Array of memcached servers. At least one server must be defined.
Each server definition is a string, with the following valid syntaxes:
hostname or IP
TCP connect to host on memcached default port
(usually 11211, defined by PHP ini variable memcache.default_port)
hostname:port
TCP connect to host on port
tcp://hostname:port
Same as above
unix:///path/to/memcached.sock
Connect to memcached server using unix sockets
compression
compression
Type
boolean
Default
false
Enable memcached internal data compression.
Can be used to reduce memcached memory consumption,
but adds additional compression / decompression CPU overhead
on the memcached servers.
Redis Backend
Redis is a key-value storage/database.
In contrast to memcached, it allows structured values.
Data is stored in RAM but it can be persisted to disk
and doesn't suffer from the design problems of the memcached backend implementation.
The redis backend can be used as an alternative to the database backend
for big cache tables and help to reduce load on database servers this way.
The implementation can handle millions of cache entries, each with hundreds of tags
if the underlying server has enough memory.
Redis is extremely fast but very memory hungry.
The implementation is an option for big caches with lots of data
because most operations perform O(1) in proportion to the number of (redis) keys.
This basically means that access to an entry in a cache with a million entries
takes the same time as to a cache with only 10 entries,
as long as there is enough memory available to hold the complete set in memory.
At the moment only one redis server can be used at a time per cache,
but one redis instance can handle multiple caches without performance loss when flushing a single cache.
Attention
The scheduler garbage collection task should be run regularly to
find and delete old cache tags entries. These do not expire on their own and
would remain in memory indefinitely - unless the cache is flushed.
The implementation is based on the PHP phpredis module,
which must be available on the system.
Warning
Please check the section on
configuration and monitor
memory usage (and eviction, if enabled). Otherwise, you may run into
problems, if not enough memory for the cache entries is reserved on the Redis
server (maxmemory).
Note
It is important to monitor the redis server and tune its settings
to the specific caching needs and hardware capabilities.
There are several articles on the net and the redis configuration file
contains some important hints on how to speed up the system if it reaches its limit.
A full documentation of available options is beyond this documentation.
Redis example
The Redis caching backend configuration is very similar to that of other
backends, with one caveat.
TYPO3 caches should be separated if the same keys are used.
This applies to the pages and pagesection caches.
Both use "tagIdents:pageId_21566" for a page with id 21566.
How you separate them is for a system administrator to decide. We provide
examples with several databases but this may not be the best option
in production where you might want to use multiple cores (which do not
support databases). Separation is also a good idea because
caches can be flushed individually.
If you have several of your own caches which each use unique keys (for example
by using a different prefix for each separate cache identifier), you can
store them in the same database, but it is good practice to separate the core
caches.
In practical terms, Redis databases should be used to separate different keys
belonging to the same application (if needed), and not to use a single Redis
instance for multiple unrelated applications.
Activate a persistent connection to a redis server. This is a good idea
in high load cloud setups.
database
database
Type
integer
Default
0
Number of the database to store entries. Each cache should have its own database,
otherwise caches sharing a database are all flushed if the flush operation
is issued to one of them. Database numbers 0 and 1 are used and flushed by the Core unit tests
and should not be used if possible.
password
password
Type
string
Password used to connect to the redis instance if the redis server needs authentication.
Warning
The password is sent to the redis server as plain text.
compression
compression
Type
boolean
Default
false
Whether or not data compression with gzip should be enabled.
This can reduce cache size, but adds some CPU overhead for the compression
and decompression operations in PHP.
compressionLevel
compressionLevel
Type
integer from -1 to 9
Default
-1
Set gzip compression level to a specific value. The default compression level is usually sufficient.
-1
Default gzip compression (recommended)
0
No compression
9
Maximum compression (but more CPU overhead)
Redis server configuration
This section is about the configuration on the Redis server, not the client.
For flushing by cache tags to work, it is important that the integrity of
the cache entries and cache tags is maintained. This may not be the case,
depending on which eviction policy (maxmemory-policy) is used. For example,
for a page id=81712, the following entries may exist in the Redis page cache:
If entries are evicted (due to memory shortage), there is no mechanism which
ensures that all related entries will be evicted. If
maxmemory-policy allkeys-lru is used, for example, this may
result in the situation that the cache entry (identData) still exists, but the
tag entry (tagIdents) does not. The tag entry reflects the relation
"cache tag => cache identifier" and is used for
RedisBackend::flushByTag()). If this entry is gone, the cache
can no longer be flushed if content is changed on the page or an explicit
flushing of the page cache for this page is requested. Once this is the case,
cache flushing (for this page) is only possible via other means (such as full
cache flush).
Because of this, the following recommendations apply:
Allocate enough memory (maxmemory) for the cache.
Use the maxmemory-policyvolatile-ttl. This will ensure
that no tagIdents entries are removed. (These have no expiration date).
Regularly run the TYPO3 scheduler garbage collection task for the Redis cache
backend.
Monitor evicted_keys in case an eviction policy is used.
Monitor used_memory if eviction policy noeviction is used. The
used_memory should always be less then maxmemory.
Tip
The information about evicted_keys etc. can be obtained via redis-cli and
the info command or via php-redis. Further information about the results of
info is in the documentation.
(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).
The file backend stores every cache entry as a single file in the file system.
The lifetime and tags are added to the file after the data section.
This backend is the big brother of the Simple file backend and implements
additional interfaces. Like the simple file backend it also implements the
PhpCapableInterface, so it can be used with
PhpFrontend. In
contrast to the simple file backend it furthermore implements
TaggableInterface and
FreezableInterface.
A frozen cache does no lifetime check and has a list of all existing cache
entries that is reconstituted during initialization. As a result, a frozen cache
needs less file system look ups and calculation time if accessing cache entries.
On the other hand, a frozen cache can not manipulate (remove, set) cache entries
anymore. A frozen cache must flush the complete cache again to make cache
entries writable again. Freezing caches is currently not used in the TYPO3 Core.
It can be an option for code logic that is able to calculate and set all
possible cache entries during some initialization phase, to then freeze the
cache and use those entries until the whole thing is flushed again. This can be
useful especially if caching PHP code.
In general, the backend was specifically optimized to cache PHP code because the
get and set operations have low overhead. The file backend is
not very good at tagging and does not scale well with the number of tags. Do
not use this backend if cached data has many tags.
Warning
The performance of flushByTag() is bad and scales just O(n).
On the contrary, performance of get() and set() operations
is good and scales well. Of course, if there are many entries, this might
still slow down after a while and a different storage strategy should be used
(e.g. RAM disks, battery backed up RAID systems or SSD hard disks).
Options for the file backend
cacheDirectory
cacheDirectory
Type
array
Default
var/cache/
The directory where the cache files are stored. By default, it is assumed
that the directory is below TYPO3_DOCUMENT_ROOT. However, an
absolute path could be selected. Every cache should be assigned
its own directory, otherwise flushing of one cache would flush all other
caches in the same directory.
Simple File Backend
The simple file backend is the small brother of the file backend. In contrast to most
other backends, it does not implement the TaggableInterface, so cache entries cannot be tagged and flushed
by tag. This improves performance if cache entries do not need such tagging. The TYPO3 Core uses this backend
for its central Core cache (it holds autoloader cache entries and other important cache entries). The Core cache is
usually flushed completely and does not need specific cache entry eviction.
PDO Backend
The PDO backend can be used as a native PDO interface to databases which are connected to PHP via PDO.
It is an alternative to the database backend if a cache should be stored in a database which is otherwise
only supported by TYPO3 dbal to reduce the parser overhead.
Garbage collection is implemented for this backend and should be called to clean up hard disk space or memory.
Note
There is currently very little production experience with this backend, especially not with a capable database like Oracle.
Any feedback for real life use cases of this cache is appreciated.
Options for the PDO backend
dataSourceName
dataSourceName
Type
string
Required
true
Data source name for connecting to the database. Examples:
mysql:host=localhost;dbname=test
sqlite:/path/to/sqlite.db
sqlite::memory
username
username
Type
string
Username for the database connection.
password
password
Type
string
Password to use for the database connection.
Transient Memory Backend
The transient memory backend stores data in a PHP array. It is only valid for one request. This is useful if code
logic carries out expensive calculations or repeatedly looks up identical
information in a database. Data is stored once in an array and data entries are retrieved
from the cache in consecutive calls, getting rid of additional overhead.
Since caches are available system-wide and shared between Core and extensions,
they can share the same information.
Since the data is stored directly in memory, this backend is the quickest. The stored data adds to
the memory consumed by the PHP process and can hit the memory_limit PHP setting.
Null Backend
The null backend is a dummy backend which doesn't store any data and always returns false
on get(). This backend is useful in a development context to "switch off" a cache.
Developer information
This chapter is aimed at extension authors who want to use the caching
framework for their needs. It is about how to use the framework properly. For
details about its inner working, please refer to the
section about architecture.
Example usages can be found throughout the TYPO3 Core, in particular in the
system extensions core and extbase.
Cache registration
Registration of a new cache should be done in an extension's
ext_localconf.php. The example below defines an empty sub-array in
cacheConfigurations. Neither frontend nor backend are defined: The cache
manager will choose the default
variable frontend and the
database backend by default.
The null coalescing assignment operator (
??=) check is used to
enable administrators to overwrite configuration of caches in
config/system/settings.php. During
bootstrap, any ext_localconf.php is loaded
afterconfig/system/settings.php and
config/system/additional.php are loaded, so it is important to make
sure that the administrator did not already set any configuration of the
extension's cache.
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.
Tip
Extensions should not force specific settings, therefore the null coalescing
assignment operator (
??=) is used to allow administrators to
overwrite those settings. It is recommended to set up a cache configuration
with sane defaults, but administrators should always be able to overwrite
them for whatever reason.
services:# Place here the default dependency injection configurationcache.myext_mycache:class:TYPO3\CMS\Core\Cache\Frontend\FrontendInterfacefactory:['@TYPO3\CMS\Core\Cache\CacheManager','getCache']arguments:['myext_mycache']
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
<?phpnamespaceMyVendor\MyExtension;
useTYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
finalclassMyClass{
publicfunction__construct(
private readonly FrontendInterface $cache,
){}
//...privatefunctiongetCachedValue(string $cacheIdentifier, array $tags, int|null $lifetime): array{
// If value is false, it has not been cached
$value = $this->cache->get($cacheIdentifier);
if ($value === false) {
// Store the data in cache
$value = $this->calculateData();
$this->cache->set($cacheIdentifier, $value, $tags, $lifetime);
}
return $value;
}
privatefunctioncalculateData(): array{
$data = [];
// todo: implementreturn $data;
}
}
Copied!
Tip
It is not needed to call
$this->cache->has() before accessing cache
entries with
$this->cache->get() as the latter returns
false
if no entry exists.
Since the auto-wiring feature of the
dependency injection container cannot detect which cache configuration should be
used for the
$cache argument of
MyClass, the container
service configuration needs to
be extended:
EXT:my_extension/Configuration/Services.yaml
services:# Place here the default dependency injection configuration# and the configuration of the cache from aboveMyVendor\MyExtension\MyClass:arguments:$cache:'@cache.myext_mycache'
The frontend cache collector API is available as a PSR-7 request attribute to
collect cache tags and their corresponding lifetime. Find more information in
the chapter Frontend cache collector.
System categories
TYPO3 provides a generic categorization system.
Categories can be created in the backend like any other type of
record.
A TCA field of the column type category is
available.
Pages, content elements and files contain category fields by default.
Using Categories
Managing Categories
System categories are defined like any other record. Each category
can have a parent, making a tree-like structure.
A category with a parent defined
The Items tab shows all related records, for example all records
that have been marked as belonging to this category.
Adding categories to a table
Categories can be added to a table by defining a TCA field of the TCA column
type category. While using this type, TYPO3
takes care of generating the necessary TCA configuration and also adds
the database column automatically. Developers only have to configure the
TCA column and add it to the desired record types:
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
relationshipmanyToMany is not supported. Therefore, the default value for this property
is oneToMany.
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 tableforeach ($collection as $item) {
$tableRecords[$item['uid']] = $item;
// Keep track of all categories a given item belongs toif (!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.
In most aspects, system categories are treated like any other record. They can
be viewed or edited by editors if they are stored in a folder where the editor
has access to and if the table
sys_category is allowed in the field
Tables (listing) and Tables (modify) in the tab
Access Lists of the user group.
Additionally it is possible to set Mounts and Workspaces >
Category Mounts in the user group. If at
least one category is set in the category mounts only the chosen categories are
allowed to be attached to records.
Code editor
Changed in version 13.0
The code editor functionality was moved from the optional "t3editor" system
extension into the "backend" system extension. The code editor is therefore
always available.
Checks whether the previous system extension is installed via
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('t3editor')
are now obsolete.
Extensions may configure backend fields to use the code editor by TCA. The
editor is only available for fields of type text. By
setting the
renderType to
codeEditor the syntax highlighting can
be activated.
By setting the property format
the mode for syntax highlighting can be chosen. Allowed values:
The following configuration options are available:
<identifier>
<identifier>
Type
string
Required
true
Represents the unique identifier and format code of the mode
(css in this example). The format code is used in TCA to
define the CodeMirror mode to be used.
Holds the JavaScriptModuleInstruction of the CodeMirror module.
extensions
extensions
Type
array
Binds the mode to specific file extensions. This is important for using
the code editor in the module File > Filelist.
default
default
Type
bool
If set, the mode is used as fallback if no sufficient mode is available.
By factory default, the default mode is html.
Console commands (CLI)
TYPO3 supports running scripts from the command line. This functionality is
especially useful for automating tasks such as cache clearing, maintenance,
and scheduling jobs.
System administrators and developers can use predefined commands to interact
with the TYPO3 installation directly from the terminal.
To learn how to run TYPO3 CLI commands in various environments (such as Composer
projects, Classic installations, DDEV, Docker, or remote servers), refer to
How to run a command.
A list of available Core commands is provided here:
List of Core console commands.
Additional commands may be available depending on installed extensions.
The extension
helhum/typo3-console
is frequently installed to
provide a wider range of CLI commands.
If you are developing your own TYPO3 extension, you can also create custom
console commands to provide functionality specific to your use case. These
commands integrate with the TYPO3 CLI and can be executed like any built-in
command.
To execute TYPO3 console commands, you need access to a terminal (command line
interface). You can run commands locally or on a remote server.
The entry point for running commands depends on the type of TYPO3 installation.
To display a list of all available commands, use the following:
vendor/bin/typo3
Copied!
typo3/sysext/core/bin/typo3
Copied!
ddev typo3
Copied!
bin/typo3
Copied!
Local development environments
If you are working on a local development environment such as DDEV, MAMP, or a
native PHP installation, open a terminal and navigate to your project
directory. Then, run the command using the appropriate entry point for your
installation (Composer or Classic mode).
cd my-typo3-project
vendor/bin/typo3 list
Copied!
Using the CLI with DDEV
If you are using DDEV, you can run TYPO3 commands from your host machine using
the ddev typo3 shortcut. This automatically routes the command into
the correct container and environment.
For example, to flush all caches:
ddev typo3 cache:flush
Copied!
You can use this shortcut with any TYPO3 CLI command:
ddev typo3 <your-command>
Copied!
Using the CLI in Docker containers
If you are using Docker directly (without DDEV or a wrapper), TYPO3 commands
must usually be executed inside the container that runs PHP and TYPO3.
First, open a shell inside the container. For example:
docker exec -it my_php_container bash
Copied!
Replace my_php_container with the name of your running PHP container.
Once inside the container, navigate to the project directory and run the
command:
cd /var/www/html
vendor/bin/typo3 list
Copied!
You typically cannot run TYPO3 commands from the host system unless the
project's PHP environment is directly accessible outside the container.
Executing commands on remote servers via SSH
For TYPO3 installations on a remote server, you typically access the server
using SSH (Secure Shell).
Use the following command to connect:
ssh username@example.com
Copied!
Replace username with your SSH username and example.com with the server
hostname or IP address.
Once logged in, navigate to your TYPO3 project directory and execute the
command. For example:
cd /var/www/my-typo3-site
vendor/bin/typo3 list
Copied!
Making the CLI entry point executable
The TYPO3 command entry point (e.g. vendor/bin/typo3) must be marked as
executable in order to run it directly.
To make the file executable, run:
chmod +x vendor/bin/typo3
Copied!
If you do not have permission to change the file mode or the file system is
read-only, you can run the script by calling the PHP interpreter explicitly:
php vendor/bin/typo3 list
Copied!
This method works even if the file is not executable.
Executing commands from the scheduler
By default, it is possible to run a command from the
TYPO3 Scheduler
as well. To do this, select the task Execute console commands
followed by your command in the Schedulable Command field.
Note
You need to save and reopen the task to define command arguments.
In continuous integration (CI) or continuous deployment (CD) pipelines, TYPO3
CLI commands can be used for tasks such as preparing the system, checking
configuration, or activating extensions (Classic mode).
The exact command entry point depends on your installation type. For example:
vendor/bin/typo3 list
Copied!
Before you can run most TYPO3 CLI commands, ensure the following:
You have run composer install so that the vendor/ directory and
CLI entry point exist.
The PHP environment is set up (with extensions such as pdo_mysql).
The environment variable TYPO3_CONTEXT is set appropriately, such as
Production or Testing.
Important
If no database is available during the pipeline (in a build-only step),
many commands will not work. For example, cache:flush requires a
database connection and will fail without one.
If you want to run commands such as cache:flush after deployment,
it is common to use the CI pipeline to connect to the remote server and
execute the command there using SSH:
The extension
helhum/typo3-console
ships many commands to execute TYPO3
actions, which otherwise would only be accessible via the TYPO3 backend.
This page assumes that the code is run on a Composer based installation with
default binaries location. Here you can read how to run them in general and
on Classic mode installations:
Run a command from the command line.
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
Options
--debug
--debug
Tail the completion debug log
Value
None allowed
Default value
false
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
The completion command dumps the shell completion script required
to use shell autocompletion (currently, bash, fish, zsh completion are supported).
Static installation
Dump the script to a global completion file and restart your shell:
vendor/vendor/bin/typo3 completion | sudo tee /etc/bash_completion.d/typo3
Copied!
Or dump the script to a local file and source it:
vendor/vendor/bin/typo3 completion > completion.sh
# source the file whenever you use the project
source completion.sh
# or add this line at the end of your "~/.bashrc" file:
source /path/to/completion.sh
Copied!
Dynamic installation
Add this to the end of your shell configuration file (e.g. "~/.bashrc"):
Variable TYPO3_DB_PASSWORD (option --password) can be used to provide a
password for the database and TYPO3_SETUP_ADMIN_PASSWORD
(option --admin-user-password) for the admin user password.
Using this can be a security risk since the password may end up in shell
history files. Prefer the interactive mode. Additionally, writing a command
to shell history can be suppressed by prefixing the command with a space
when using bash or zsh.
If set, a locked TYPO3 Backend will redirect to URI specified with this argument. The URI is saved as a string in the lockfile that is specified in the system configuration.
Variable "TYPO3_BE_USER_PASSWORD" and options "-p" or "--password" can be
used to provide a password. Using this can be a security risk since the password
may end up in shell history files. Prefer the interactive mode. Additionally,
writing a command to shell history can be suppressed by prefixing the command
with a space when using bash or zsh.
If this option is set, the references will not be removed, but just the output which references would be deleted are shown
Value
None allowed
Default value
false
--update-refindex
--update-refindex
Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode
Value
None allowed
Default value
false
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
Assumptions:
- a perfect integrity of the reference index table (always update the reference index table before using this tool!)
- all database references to check are integers greater than zero
- does not check if a referenced record is inside an offline branch, another workspace etc. which could make the reference useless in reality or otherwise question integrity
Records may be missing for these reasons (except software bugs):
- someone deleted the record which is technically not an error although it might be a mistake that someone did so.
- after flushing published versions and/or deleted-flagged records a number of new missing references might appear; those were pointing to records just flushed.
An automatic repair is only possible for managed references are (not for soft references), for
offline versions records and non-existing records. If you just want to list them, use the --dry-run option.
The references in this case are removed.
If the option "--dry-run" is not set, all managed files (TCA/FlexForm attachments) will silently remove the references
to non-existing and offline version records.
All soft references with relations to non-existing records, offline versions and deleted records
require manual fix if you consider it an error.
Manual repair suggestions:
- For soft references you should investigate each case and edit the content accordingly.
- References to deleted records can theoretically be removed since a deleted record cannot be selected and hence
your website should not be affected by removal of the reference. On the other hand it does not hurt to ignore it
for now. To have this automatically fixed you must first flush the deleted records after which remaining
references will appear as pointing to Non Existing Records and can now be removed with the automatic fix.
If you want to get more detailed information, use the --verbose option.
If this option is set, the records will not be deleted. The command outputs a list of broken records
Value
None allowed
Default value
false
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
TYPO3 "pages" database table is a tree that represents a hierarchical structure with a set of connected nodes by their "uid" und "pid" values.
All TCA records must be connected to a valid "pid".
This command finds and deletes all "pages" rows that do not have a proper tree connection to "pid" "0".
It also finds and deletes TCA record rows having a "pid" set to invalid "pages" "uid"s.
The command can be called using "typo3 cleanup:orphanrecords -v --dry-run" to find affected records allowing manual inspection.
Find all versioned records and possibly cleans up invalid records in the database.
Usage
vendor/bin/typo3 cleanup:previewlinks
Copied!
Options
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
Look for preview links within the database table "sys_preview" that have been expired and and remove them. This command should be called regularly when working with workspaces.
Setting start page in page tree. Default is the page tree root, 0 (zero)
Value
Required
--depth
--depth / -d
Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.
Value
Required
--dry-run
--dry-run
If this option is set, the records will not actually be deleted/modified, but just the output which records would be touched are shown
Value
None allowed
Default value
false
--action
--action
Specify which action should be taken. Set it to "versions_in_live", "published_versions" or "invalid_workspace"
Value
Optional
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
Traverse page tree and find versioned records. Also list all versioned records, additionally with some inconsistencies in the database, which can cleaned up with the "action" option. If you want to get more detailed information, use the --verbose option.
If set, existing records with the same UID will be updated instead of inserted.
Value
None allowed
Default value
false
--ignore-pid
--ignore-pid
If set, page IDs of updated records are not corrected (only works in conjunction with --update-records).
Value
None allowed
Default value
false
--force-uid
--force-uid
If set, UIDs from file will be forced.
Value
None allowed
Default value
false
--import-mode
--import-mode
Set the import mode of this specific record. Pattern is "{table}:{record}={mode}". Available modes for new records are "force_uid" and "exclude" and for existing records "as_new", "ignore_pid", "respect_pid" and "exclude". Examples are "pages:987=force_uid", "tt_content:1=as_new", etc.
Value
Optional (multiple)
Default value
[]
--enable-log
--enable-log
If set, all database actions are logged.
Value
None allowed
Default value
false
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
Imports a T3D / XML file with content into a page tree
UID of a specific task. Can be provided multiple times to execute multiple tasks sequentially.
Value
Optional (multiple)
Default value
[]
--force
--force / -f
Force execution of the task which is passed with --task option
Value
None allowed
Default value
false
--stop
--stop / -s
Stop the task which is passed with --task option
Value
None allowed
Default value
false
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
If no parameter is given, the scheduler executes any tasks that are overdue to run.
Call it like this: typo3/sysext/core/vendor/bin/typo3 scheduler:run --task=13 -f
vendor/bin/typo3 setup:begroups:default
vendor/bin/typo3 setup:begroups:default Back to list
Run upgrade wizard. Without arguments all available wizards will be run.
Usage
vendor/bin/typo3 upgrade:run [<wizardName>]
Copied!
Arguments
wizardName
wizardName
Options
--silent
--silent
Do not output any message
Value
None allowed
Default value
false
Help
This command allows running upgrade wizards on CLI. To run a single wizard add the identifier of the wizard as argument. The identifier of the wizard is the name it is registered with in ext_localconf.
Some workspaces can have an auto-publish publication date to put all "ready to publish" content online on a certain date.
Writing custom commands
TYPO3 uses the Symfony Console component to define and execute command-line
interface (CLI) commands. Custom commands allow extension developers to
provide their own functionality for use on the command line or in the TYPO3
scheduler.
To implement a console command in TYPO3 extend the
\Symfony\Component\Console\Command\Command class.
See also
Console commands in TYPO3 are based on the same technology as
commands in Symfony. Find more information about
Commands in the Symfony documentation.
Console command registration
There are two ways that a console command can be registered: you can use the
PHP Attribute AsCommand or register the command in your Services.yaml:
PHP attribute AsCommand
CLI commands can be registered by setting the attribute
\Symfony\Component\Console\Attribute\AsCommand in the command class.
When using this attribute there is no need to register the command in
Services.yaml.
#[AsCommand(
name: 'examples:dosomething',
description: 'A command that does nothing and always succeeds.',
aliases: ['examples:dosomethingalias'],
)]
Copied!
The following parameters are available:
name
The name under which the command is available.
description
Gives a short description. It will be displayed in the list of commands and
the help information for the command.
hidden
Hide the command from the command list by setting
hidden to
true.
alias
A command can be made available under a different name. Set to
true
if your command name is an alias.
If you want to set a command as non-schedulable
it has to be registered via tag not attribute.
Tag console.command in the Services.yaml
You can register the command in Configuration/Services.yaml by adding the service
definition of your class as a tag
console.command:
packages/my_extension/Configuration/Services.yaml
services:# ...MyVendor\MyExtension\Command\DoSomethingCommand:tags:-name:console.commandcommand:'examples:dosomething'description:'A command that does nothing and always succeeds.'# Also an alias for the command can be configured-name:console.commandcommand:'examples:dosomethingalias'alias:true
Copied!
Note
Despite using autoconfigure: true the commands
have to be explicitly defined in Configuration/Services.yaml. It
is recommended to always supply a description, otherwise there will be
an empty space in the list of commands.
Making a command non-schedulable
A command can be set as disabled for the scheduler by setting
schedulable
to
false. This can only be done when registering the command via
tag and not via attribute:
packages/my_extension/Configuration/Services.yaml
services:# ...MyVendor\MyExtension\Command\DoSomethingCommand:tags:-name:console.commandcommand:'examples:dosomething'description:'A command that does nothing and cannot be scheduled.'schedulable:false
Copied!
Context of a command: No request, no site, no user
Commands are called from the console / command line and not through a web
request. Therefore, when the code of your custom command is run by default there
is no ServerRequest
available, no backend or frontend user logged in and a request is called without
context of a site or page.
For that reason Site Settings, TypoScript and TSconfig are not loaded by default,
Extbase repositories cannot be used without taking precautions and there are many
more limitations.
Extbase limitations in CLI context
Attention
It is not recommended to use Extbase repositories in a
CLI context.
This is necessary when using the DataHandler
or other backend permission-handling-related tasks.
Simulating a frontend request in TYPO3 Commands
Executing a TYPO3 command in the CLI does not trigger a frontend (web)
request. This means that several request attributes required for link generation
via Fluid or TypoScript are missing by default. While setting the site
attribute in the request is a first step, it does not fully replicate the
frontend behavior.
<?phpdeclare(strict_types=1);
namespaceT3docs\Examples\Command;
useSymfony\Component\Console\Attribute\AsCommand;
useSymfony\Component\Console\Command\Command;
useSymfony\Component\Console\Input\InputInterface;
useSymfony\Component\Console\Output\OutputInterface;
useTYPO3\CMS\Core\Core\Bootstrap;
useTYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
useTYPO3\CMS\Core\Http\ServerRequest;
useTYPO3\CMS\Core\Mail\FluidEmail;
useTYPO3\CMS\Core\Mail\MailerInterface;
useTYPO3\CMS\Core\Site\SiteFinder;
#[AsCommand(
name: 'myextension:sendmail',
)]
classSendFluidMailCommandextendsCommand{
publicfunction__construct(
private readonly SiteFinder $siteFinder,
private readonly MailerInterface $mailer,
){
parent::__construct();
}
protectedfunctionexecute(InputInterface $input, OutputInterface $output): int{
Bootstrap::initializeBackendAuthentication();
// The site has to have a fully qualified domain name
$site = $this->siteFinder->getSiteByPageId(1);
$request = (new ServerRequest())
->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
->withAttribute('site', $site);
$GLOBALS['TYPO3_REQUEST'] = $request;
// Send some mails with FluidEmail
$email = new FluidEmail();
$email->setRequest($request);
// Set receiver etc$this->mailer->send($email);
return Command::SUCCESS;
}
}
Copied!
Note
Simulating a frontend request in CLI is possible but requires
all bootstrapping steps to be manually carried out. While basic functionality,
such as link generation, can work with a minimal setup, complex
TypoScript-based link modifications, access restrictions, and
context-aware rendering require additional configuration and may still
behave differently from a real web request.
Create a command with arguments and interaction
Passing arguments
Since a command extends
\Symfony\Component\Console\Command\Command,
it is possible to define arguments (ordered) and options (unordered) using the Symfony
command API. This is explained in depth on the following Symfony Documentation page:
Both arguments and properties can be registered in a command implementation by
overriding the configure() method. You can call methods addArgument() and
addOption() to register them.
This argument can be retrieved with
$input->getArgument(), the options with
$input->getOption(), for example:
<?phpdeclare(strict_types=1);
namespaceT3docs\Examples\Command;
useSymfony\Component\Console\Attribute\AsCommand;
useSymfony\Component\Console\Command\Command;
useSymfony\Component\Console\Input\InputInterface;
useSymfony\Component\Console\Output\OutputInterface;
useTYPO3\CMS\Core\Core\Bootstrap;
useTYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
useTYPO3\CMS\Core\Http\ServerRequest;
useTYPO3\CMS\Core\Mail\FluidEmail;
useTYPO3\CMS\Core\Mail\MailerInterface;
useTYPO3\CMS\Core\Site\SiteFinder;
#[AsCommand(
name: 'myextension:sendmail',
)]
classSendFluidMailCommandextendsCommand{
publicfunction__construct(
private readonly SiteFinder $siteFinder,
private readonly MailerInterface $mailer,
){
parent::__construct();
}
protectedfunctionexecute(InputInterface $input, OutputInterface $output): int{
Bootstrap::initializeBackendAuthentication();
// The site has to have a fully qualified domain name
$site = $this->siteFinder->getSiteByPageId(1);
$request = (new ServerRequest())
->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
->withAttribute('site', $site);
$GLOBALS['TYPO3_REQUEST'] = $request;
// Send some mails with FluidEmail
$email = new FluidEmail();
$email->setRequest($request);
// Set receiver etc$this->mailer->send($email);
return Command::SUCCESS;
}
}
Copied!
Content Elements & Plugins
This chapter handles content elements & plugins: What they are, how they can be
created, how existing content elements or plugins can be customized etc.
In TYPO3, Content elements and plugins are both stored as Database records
in table
tt_content. They are usually edited in the backend in module
Web > Page.
Content elements and plugins are both used to present and manage
content on a website, but they serve different purposes and have distinct
characteristics:
A content element is a standard unit for managing and displaying content,
such as text, images, videos, tables, and more.
TYPO3 provides a variety of built-in content elements. It is possible
to define custom content elements.
A plugin in TYPO3 is more complex, typically providing dynamic
or interactive functionality. Plugins are usually provided by extensions
that introduce new features to the website.
The data to be displayed is usually supplied by a special PHP class
called a "controller". Depending on the technology used in the controller
the plugin can be an Extbase plugin or a plain plugin.
Content elements in TYPO3
A content element is a standard unit for managing and displaying content,
such as text, images, videos, tables, and more.
In the TYPO3 backend, content elements are commonly managed in module
Web > Page.
From a technical point of view content elements are records stored in the
database table tt_content. Each content
element has a specific content element type, specified by the database field
tt_content.CType. This type influences both the backend form and the frontend
output.
The appearance of a content element in the backend form is defined via the
TYPO3 Configuration Array (TCA) of table tt_content.
Each content element type is configured by one entry in the section
$TCA['types'].
The output of the content element in the frontend is configured by an entry in
the TypoScript top-level object tt_content using the
same key as in TCA. In most cases a FLUIDTEMPLATE
is used delegating the actual output to the templating engine
Fluid.
A content element can be of a type
supplied by TYPO3, such as textmedia (text with or without images or videos).
Or it can have a custom type supplied by an extension such as carousel
provided by the
bk2k/bootstrap-package
extension.
Adding custom content elements is
possible without writing PHP code and can therefore also be done by
TYPO3 integrators.
Plugins in TYPO3
A plugin in TYPO3 is a more complex implementation, typically providing dynamic
or interactive functionality. Plugins are usually provided by extensions
that introduce new features to the website.
The data to be displayed is usually supplied by a special PHP class
called a "controller". Depending on the technology used in the controller
the plugin can be an Extbase plugin or a plain plugin.
Extbase plugins
For usage in the TYPO3 backend Extbase plugins are registered with utility
functions of class
\TYPO3\CMS\Extbase\Utility\ExtensionUtility (not to
be confused with
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility ).
Setting the fifth parameter to any value but ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
is deprecated. See Migration: list_type plugins to CType.
Method
ExtensionUtility::configurePlugin() also takes care of registering
the plugin for frontend output in TypoScript using an object of type
EXTBASEPLUGIN.
If it is desired that editors can insert the Extbase plugin like a content
element into the page it also needs to be registered with
ExtensionUtility::registerPlugin() in the TCA Overrides, for example file
EXT:my_extension/Configuration/TCA/Overrides/tt_content.php:
It is possible to create a plugin without using Extbase by creating a plain PHP
class as a controller.
In this case you have to define the TypoScript configuration yourself. A
USER or USER_INT TypoScript object can be used
to delegate the rendering to your controller:
Plugins often use additional database tables which contain records which are
dynamically displayed via the plugin - often in a list view, a single view,
optionally with pagination and search functionality. An extension may provide
several plugins, each with a dedicated function, such as the list view.
Plugins are often used if more complex functionality is required (than in non-
plugin content elements)
Plugins can be created using the Extbase framework or by Core functionality.
A typical extension with plugins is the
georgringer/news
extension
which comes with plugins to display news records in lists or as a single view
with only one news record.
The news records are stored in a custom database table (tx_news_domain_model_news)
and can be edited in the backend.
There are also system extensions that have plugins.
typo3/cms-felogin
has a plugin that allow frontend users, stored in table fe_users to log into
the website.
typo3/cms-indexed-search
has a plugin that can be
used to search in the index and display search results.
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:
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.
Starting with TYPO3 13.0 content elements added via TCA are automatically
displayed in the New Content Element wizard. To stay compatible
with both TYPO3 v12.4 and v13 keep the page TSconfig for TYPO3 v12.4.
See New Content Element wizard in TYPO3 12.4.
<htmldata-namespace-typo3-fluid="true"xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"><h2>Data available to the content element: </h2><f:debuginline="true">{_all}</f:debug><h2>Output</h2><div><f:format.html>{data.bodytext}</f:format.html></div></html>
Copied!
All fields of the table
tt_content are now available in the variable
data. Read more about Fluid.
Tip
During development you can output all available variables in a Fluid
template by adding
<f:debug>{_all}</f:debug>.
Even more convenient:
<f:if condition="{condition}"><f:debug>{_all}</f:debug></f:if>
lets you easily turn debugging on or off, depending on whether you
fill in "1" or "0" for condition.
Below you can see the example output of the new content element and a
dump of all available data:
Extended example: Extend tt_content and use data processing
You can find the complete example in the TYPO3 Documentation Team extension
t3docs/examples
. The steps for
creating a simple new content element as above need to be repeated. We use the
key examples_newcontentcsv in this example.
We want to output comma separated values (CSV) stored in the field bodytext.
As different programs use different separators to store CSV we want to make
the separator configurable.
Extending tt_content
If the available fields in the table tt_content are not sufficient you can add
your own fields. In this case we need a field
tx_examples_separator from
which to choose the desired separator.
Extending the database schema
First we extend the database schema by adding the following to the file
ext_tables.sql:
Do a database compare in the Admin Tools > Maintenance module
after changing the database schema (system maintainers only). Or call the
console command:
You can read more about defining fields via TCA in the TCA Reference.
Now the new field can be used in your Fluid template just like any other
tt_content field.
Another example shows the connection to a foreign table. This allows you to be
more flexible with the possible values in the select box. The new field
myextension_reference is a reference to another table of the extension
called
tx_myextension_mytable:
An individual modification of the newly added field
myextension_reference
to the TCA definition of the table
tt_content can be done in the
TYPO3 Core Engine (TCE) page TSconfig. In most cases it is necessary to set the page
id of the general storage folder. Then the examples extension will only use
the content records from the given page id.
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###):
<htmldata-namespace-typo3-fluid="true"xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"><h2>Data in variable "myTable"</h2><f:debuginline="true">{myTable}</f:debug><h2>Output, {data.imagecols} columns separated by char {data.tx_examples_separator}</h2><tableclass="table table-hover"><f:foreach="{myTable}"as="columns"iteration="i"><tr><thscope="row">{i.cycle}</th><f:foras="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):
Custom data processors
When there is no suitable data processor that
prepares the variables needed for your content element or template, you can
define a custom data processor by implementing
\TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface .
You can find the example below in the TYPO3 Documentation Team extension
t3docs/examples
.
Using a custom data processor in TypoScript
The data processor can be configured through a TypoScript setup configuration. A
custom data processor can be used in the definition of a "new custom content
element" as follows:
In the extension examples you can find the code in
EXT:examples/Configuration/TypoScript/DataProcessors/Processors/CustomCategoryProcessor.typoscript.
In the field
categories the comma-separated categories are stored.
Note
The custom data processor described here should serve as a simple example.
It can therefore only work with comma-separated values, not with an m:n
relationship as used in the field
categories of tables like
tt_content. For that, further logic would need to be implemented.
Register an alias for the data processor (optional)
New in version 12.1
Instead of using the fully-qualified class name as data processor identifier
(in the example above
\T3docs\Examples\DataProcessing\CustomCategoryProcessor)
you can also define a short alias in Configuration/Services.yaml:
The alias
custom-category can now be used as data processor identifier
like in the TypoScript example above.
Note
When registering a data processor alias please be sure you don't override
an existing alias (from TYPO3 Core or a third-party extension) as this may
cause errors.
Tip
It is recommended to tag custom data processors as this will
automatically add them to the internal
DataProcessorRegistry,
enabling dependency injection by default.
Otherwise, the service would need to be set public.
Note
If your data processor should not be shared then
you need to set the
shared: false tag attribute for the service.
Implementing the custom data processor
The custom data processor must implement
\TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface .
The main method
process() gets called with the following parameters:
ContentObjectRenderer $cObj
Receives the data of the current TypoScript context, in this case the
data of the calling content element.
array $contentObjectConfiguration
Contains the configuration of the calling content element. In this example it is
the configuration
tt_content.examples_dataproccustom
array $processorConfiguration
Contains the configuration of the currently called data processor. In this
example it is the value of
as and the
stdWrap
configuration of the
categoryList
array $processedData
On calling, contains the processed data of all previously called data
processors on this same content element. Your custom data processor also stores
the variables to be sent to the Fluid template here.
This is an example implementation of a custom data processor:
<?phpdeclare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project. [...]
*/namespaceT3docs\Examples\DataProcessing;
useT3docs\Examples\Domain\Repository\CategoryRepository;
useTYPO3\CMS\Core\Utility\GeneralUtility;
useTYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
useTYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Class for data processing comma separated categories
*/classCustomCategoryProcessorimplementsDataProcessorInterface{
/**
* Process data for the content element "My new content element"
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array<string, mixed> $contentObjectConfiguration The configuration of Content Object
* @param array<string, mixed> $processorConfiguration The configuration of this processor
* @param array<string, mixed> $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array<mixed> the processed data as key/value store
*/publicfunctionprocess(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData,
){
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// categories by comma separated list
$categoryIdList = $cObj->stdWrapValue('categoryList', $processorConfiguration);
$categories = [];
if ($categoryIdList) {
$categoryIdList = GeneralUtility::intExplode(',', (string)$categoryIdList, true);
/** @var CategoryRepository $categoryRepository */
$categoryRepository = GeneralUtility::makeInstance(CategoryRepository::class);
foreach ($categoryIdList as $categoryId) {
$categories[] = $categoryRepository->findByUid($categoryId);
}
// set the categories into a variable, default "categories"
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'categories');
$processedData[$targetVariableName] = $categories;
}
return $processedData;
}
}
Copied!
In the extension examples you can find the code in
EXT:/examples/Classes/DataProcessing/CustomCategoryProcessor.php.
On being called, the
CustomCategoryProcessor runs
stdWrap
on the calling ContentObjectRenderer, which has the data of the table
tt_content in the calling content element.
The field
categoryList gets configured in TypoScript as follows:
categoryList.field = categories
Copied!
stdWrap fetches the value of
categoryList from
tt_content.tx_examples_main_category of the currently calling content
element.
Now the custom data processor processes the comma-separated values into an array
of integers that represent uids of the table
sys_category. It then
fetches the category data from the
CategoryRepository by calling
findByUid.
The data of the category records then get stored in the desired key in the
$processedData array.
To make the data processor more configurable, we test for a TypoScript
if condition at the beginning, and name the key
we use to store the data configurable by the configuration
as.
Create plugins
How to create plugins with the Extbase framework and Fluid templating engine
is handled in depth in the chapter
Registration of frontend plugins.
There are basically two ways to create frontend plugins in TYPO3:
Create a plugin using Core functionality (without Extbase) and a custom controller
Generally speaking, if you already use Extbase, it is good practice to
create your plugins using the Extbase framework. This also involves:
creating controller actions
create a domain model and repository (if your plugin requires records
that are persisted in the database)
create a view using Fluid templates
Configure custom backend preview for content element
To allow editors a smoother experience, all custom content elements and plugins
should be configured with a corresponding backend preview that shows an
approximation of the element's appearance in the TYPO3 page module. The
following sections describe how to achieve that.
A preview renderer is used to facilitate (record) previews in TYPO3. This
class is responsible for generating the preview and the wrapping.
The default preview renderer is
\TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer
and handles the Core's built-in content types (field
CType in table
tt_content).
Extend the default preview renderer
There are two ways to provide previews for your custom content types:
via page TSconfig or event listener.
Page TSconfig
This is the "integrator" way, no PHP coding is required. Just some page TSconfig
and a Fluid template.
This requires at least some PHP coding, but allows more flexibility in
accessing and processing the content elements properties.
New in version 12.0
Since version 12.0 this technique replaces the former hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']
The event
PageContentPreviewRenderingEvent is being dispatched by the
StandardContentPreviewRenderer. You can listen to it with your own
event listener.
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.
Dedicated method for rendering preview header HTML for
the page module only. Receives the GridColumnItem
that contains the record for which a preview header
should be rendered and returned.
Dedicated method for rendering preview body HTML for
the page module only. Receives the GridColumnItem
that contains the record for which a preview should be
rendered and returned.
Dedicated method for wrapping a preview header and body
HTML. Receives $item, an instance of GridColumnItem holding
among other things the record, which can be used to determine
appropriate wrapping.
param $previewHeader
the previewHeader
param $previewContent
the previewContent
param $item
the item
Returns
string
Implementing these methods allows you to control the exact composition of the
preview.
This means assuming your preview renderer returns
<h4>Header</h4>
from the header render method and
<p>Body</p> from the preview content
rendering method and your wrapping method does
return '<div>' . $previewHeader . $previewContent . '</div>'; then the
entire output becomes
<div><h4>Header</h4><p>Body</p></div> when
combined.
Should you wish to reuse parts of the default preview rendering and only change,
for example, the method that renders the preview body content, you can extend
\TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer in your custom
preview renderer class - and selectively override the methods from the API
displayed above.
Configuring the implementation
Individual preview renderers can be defined by using one of the following
approaches:
The recommended location is in the
ctrl array in your extension's Configuration/TCA/$table.php
or Configuration/TCA/Overrides/$table.php file. The former is used
when your extension is the one that creates the table, the latter is used
when you need to override TCA properties of tables added by the Core or
other extensions.
Note
The content elements
text,
textpic,
textmedia and
image have their own
PreviewRenderer. Therefore it's not
sufficient to overwrite the
StandardContentPreviewRenderer but
you need to use the second approach from above for every single of
these content elements.
New content element wizard
Changed in version 13.0
Custom content element types are auto-registered for the
New Content Element wizard. The listing can be configured using
TCA.
The content element wizard opens when a new content element is
created. It can be fully configured using ref:Page TSconfig <t3tsref:pagetsconfig>.
The wizard looks like this:
The title can be a string or, recommended, a language reference.
The description can be a string or, recommended, a language reference.
The group can be one of the existing group identifiers or a new one.
The icon can be one of the existing registered icon keys or a custom
icon key registered in the icon API.
Any of these entries can be omitted. You should at
least define a title.
New content elements are usually added in extensions in file
EXT:my_extension/Configuration/Overrides/tt_content.php.
The following groups are available by default:
default
Changed in version 13.0 This group was renamed from group `common`.
Default group for commonly used content elements
forms
Content elements representing forms like a contact form or a login form
lists
menu
Menus that can be inserted as content elements like a sitemap or a menu
of all subpages.
plugins
Plugins provided by extensions
special
Content elements that are used of special cases
All content element groups are listed in
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups'] you can
debug them in the TYPO3 backend using the backend module
System > Configuration if
typo3/cms-lowlevel
is installed
and you are an administrator.
You can add a content element or plain plugin (no Extbase) using method
ExtensionManagementUtility::addPlugin():
of class
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility .
The key value in the parameter $itemArray is used as key of the newly added
content element representing the plugin.
When you are using CType for parameter $type the content
element is added to the select item list of column CType in table tt_content.
Deprecated since version 13.4
Using the default list_type for the parameter is deprecated. All content
elements and plugins should be added with string CType for parameter $type.
This method supplies some default values:
group
Defaults to default
icon
Icon of the extension if defined
While it is still possible to use
ExtensionManagementUtility::addTcaSelectItem()
as is commonly seen in older extensions this method is not specific to content
elements and therefore sets not default values for group and icon.
Plugins (Extbase) in the "New Content Element" wizard
To add an Extbase plugin you can use ExtensionManagementUtility::registerPlugin
of class
\TYPO3\CMS\Extbase\Utility\ExtensionManagementUtility.
This method is only available for Extbase plugins defined via
ExtensionUtility::configurePlugin in file EXT:my_extension/ext_localconf.php
The TCA is always set globally for the complete TYPO3 installation. If you have
a multi-site installation and want to alter the appearance of content elements
in the wizard or remove certain content elements this can be done via
page TSconfig.
This is commonly done on a per site basis so you can use the Site set page TSconfig provider
in your site package.
This removes the content element "Plain HTML" from the group special.
Note
The affected content elements are only removed from the specified group and
only in the wizard. Editors can still switch the CType selector to create
such a content element. The content element might also appear in another tab.
mod.wizards.newContentElement.wizardItems {
special.html {
title = Plain HTML(use with care!!)
description (
Attention: This HTML is output unsanized! The editor is responsible
to only use safe HTML content.
)
icon = mysitepackage_htmlattention
}
}
Copied!
Register a new group in the "New Content Element" wizard
New groups are added on the fly, however it is recommended to set a localized
header:
Content elements compatible with TYPO3 v12.4 and v13
If your extension supplies content elements or plugins and supports both TYPO3
v12.4 and v13 you can keep the Page TsConfig for the New Content Element
Wizard while you additionally supply
the TCA settings for TYPO3 v13.
You should use the same content element group for both definition ways or
the content element will be displayed twice, once in each group. Group common
is automatically migrated to default for TYPO3 v13.
Best practices
Following are some good practices for creating custom content element types and
plugins and for customizing content elements for usage in the backend.
Coding / structure
Use a sitepackage extension to maintain your site customization (such as
backend layouts, custom content elements etc.)
How you structure your extensions depends a little on the use case and if
they will be reused in several projects and / or made public. If you create one
extension for every custom content element, you may want to think about
whether they might be merged into one extension.
Do not use deprecated functionality. Read the Core Changelog
to check for deprecations and breaking changes between TYPO3 versions.
Some naming conventions are described in the chapter Naming conventions.
Make it easier for your editors by hiding the following by
configuration
content elements that should not be used in the "Content Element Wizard"
fields that should not be filled out in the backend forms.
Migration: list_type plugins to CType
Deprecated since version 13.4
The plugin content element (
list) and the plugin sub types
field (
list_type) have been marked as deprecated in TYPO3 v13.4 and
will be removed in TYPO3 v14.0.
Several steps are important in the migration from list_type plugins to CType
plugins:
Register plugins using the CType record type
Create update wizard which extends
\TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate
and add
list_type to
CType mapping for each plugin to migrate.
Migrate possible FlexForm registration and add dedicated
showitem TCA
configuration
Migrate possible PreviewRenderer registration in TCA
Adapt possible content element wizard items in page TSconfig, where
list_type is used
Adapt possible content element restrictions in backend layouts or container
elements defined by third-party extensions like EXT:content_defender
Extbase plugins are usually registered using the utility method
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() in file
EXT:my_extension/ext_localconf.php.
Add value ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT as fifth parameter,
$pluginType, to the method
ExtensionUtility::configurePlugin():
EXT:examples/ext_localconf.php (difference)
use T3docs\Examples\Controller\FalExampleController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
ExtensionUtility::configurePlugin(
'Examples',
'HtmlParser',
[
HtmlParserController::class => 'index',
],
+ [],+ ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
);
Copied!
If the fourth parameter, $nonCacheableControllerActions was missing you can
set it to an empty array, the default value.
It is theoretically possible that the extension author did not use this utility
method. In that case you have to change the TypoScript used to display your
plugin manually. This step is similar to adjusting the TypoScript of a
Core-based plugin.
2. Adjust the registration of FlexForms and additional fields
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Extbase\Utility\ExtensionUtility; $extensionKey = 'Examples'; $pluginName = 'HtmlParser'; $pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:htmlparser_plugin_title'; // Register the HTML Parser plugin $pluginSignature = ExtensionUtility::registerPlugin( $extensionKey, $pluginName, $pluginTitle, );-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]- = 'layout,select_key,pages';-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature]- = 'pi_flexform';--ExtensionManagementUtility::addPiFlexFormValue(- $pluginSignature,- 'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',-);+ExtensionManagementUtility::addToAllTCAtypes(+ 'tt_content',+ '--div--;Configuration,pi_flexform,',+ $pluginSignature,+ 'after:subheader',+);++ExtensionManagementUtility::addPiFlexFormValue(+ '*',+ 'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',+ $pluginSignature,+);
Copied!
The CType based plugin does not inherit the default fields provided by the
TCA of the content element "List". These where in many cases removed by
using subtypes_excludelist.
As these fields are not displayed automatically anymore you can remove this
definition without replacement: Line 15 in the diff. If they have not been
removed and are still needed, you will need to manually add them to your plugin type.
The subtypes_addlist was used to
display the field containing the FlexForm, an possibly other fields in the
list_type plugin. We remove this definition (Line 17) and replace it
by using the utility method
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes()
(Line 25-30).
The utility method
ExtensionManagementUtility::addPiFlexFormValue()
needs to be changed from using the first parameter for the $pluginSignature
to using the third parameter. The first parameter requires a certain list_type
setting it to the wildcard * allows all list types. The third parameter limits
it to the CType.
You can extend class
AbstractListTypeToCTypeUpdate
to provide a custom upgrade wizard that moves existing plugins from the
list_type definition to the CType definition. The resulting upgrade wizard
will even adjust backend user permissions for the defined plugins:
Class T3docs\Examples\Upgrades\ExtbasePluginListTypeToCTypeUpdate
4. Search your code and replace any mentioning of list_type
Search your code. If you used the list_type of your plugin in any custom
database statement or referred to the according key within TypoScript,
you will need to do the possible proper replacement.
Search your TCA definitions for any use of the now outdated configuration
options.
Note, that if your old key was something like "example_pi1" you are not
forced to set the CType to "ExamplePi1" with UpperCamelCase,
but you could keep the old identifier. This makes replacements less time consuming.
$pluginSignature = 'examples_pi1';
$pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.examples_pi1.title';
$extensionKey = 'examples';
$flexFormPath = 'FILE:EXT:examples/Configuration/Flexforms/flexform_ds1.xml';
// Add the plugins to the list of plugins
ExtensionManagementUtility::addPlugin(
[$pluginTitle, $pluginSignature, '', 'plugin'],
- 'list_type',+ 'CType',
$extensionKey,
);
-// Disable the display of layout and select_key fields for the plugin-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]- = 'layout,select_key,pages';--// Activate the display of the plug-in flexform field and set FlexForm definition-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature] = 'pi_flexform';+// Activate the display of the FlexForm field+ExtensionManagementUtility::addToAllTCAtypes(+ 'tt_content',+ '--div--;Configuration,pi_flexform,',+ $pluginSignature,+ 'after:subheader',+);
ExtensionManagementUtility::addPiFlexFormValue(
- $pluginSignature,+ '*',
$flexFormPath,
+ $pluginSignature,
);
Copied!
2. Adjust the TypoScript of the plugin
If your plugin was rendered using
typo3/cms-fluid-styled-content
you are
probably using the top level TypoScript object
tt_content to render the plugin. The path to
the plugin rendering needs to be adjusted as you cannot use the deprecated content
element "list" anymore:
-tt_content.list.20.examples_pi1 = USER-tt_content.list.20.examples_pi1 {+tt_content.examples_pi1 =< lib.contentElement+tt_content.examples_pi1 {
20 = USER
20 {
userFunc = MyVendor\Examples\Controller\ExampleController->example
settings {
singlePid = 42
listPid = 55
}
view {
templateRootPaths.10 = {$templateRootPath}
partialRootPaths.10 = {$partialRootPath}
layoutRootPaths.10 = {$layoutRootPath}
}
}
templateName = Generic
}
# Or if you used the plugin top level object:
-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
Copied!
3. Provide an upgrade wizard for automatic content migration for TYPO3 v13.4 and v12.4
If you extension only support TYPO3 v13 and above you can extend the Core class
\TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate .
If your extension also supports TYPO3 v12 and maybe even TYPO3 v11 you can use
class
\Linawolf\ListTypeMigration\Upgrades\AbstractListTypeToCTypeUpdate
instead. Require via composer:
linawolf/list-type-migration
or
copy the file into your extension using your own namespaces:
Content Security Policy (CSP) is a security standard introduced to prevent
cross-site scripting (XSS), clickjacking and other code
injection attacks resulting of malicious content being executed in the trusted
web page context.
Think of CSP in terms of an "allow/deny" list for remote contents.
See also
If you are not familiar with Content Security Policy, please read the
following resources:
CSP rules are used to describe, which external assets or functionality are allowed for
certain HTML tags (like
<script>,
<img>,
<iframe>). This allows
to restrict external resources or JavaScript execution with security in mind. When
accessing a page, these rules are sent as part of the HTTP request from the server to
the browser, and the browser will enforce these rules (and reject non-allowed content).
These rejection are always logged in the browser console. Additionally, external tools
can be configured to receive and track violations of the policy.
Content Security Policy declarations can be applied to a TYPO3 website in
frontend and backend scope with a dedicated API. This API allows for site-specific or
extension-specific configuration instead of manually setting the CSP rules with
server-side configuration through httpd.conf/nginx.conf or .htaccess files.
To delegate Content Security Policy handling to the TYPO3 frontend, at least one of
the feature flags:
enforce:inheritDefault:true# site-specific mutations could also be listed, like this:# mutations:# - mode: "append"# directive: "img-src"# sources:# - "cdn.example.com"# - "assets.example.com"# alternatively (or additionally!), reporting can be set too,# for example when testing stricter rules than above# (note the missing 'assets.example.com')# report:# inheritDefault: true# mutations:# - mode: "append"# directive: "img-src"# sources:# - "cdn.example.com"#
Copied!
Changed in version 13.0
In the TYPO3 backend the Content Security Policy is always enforced.
Within the TYPO3 backend, a specific backend module is available to inspect policy
violations / reports, and there is also a list to see all configured CSP rules,
see section Active content security policy rules.
Terminology
This document will use very specific wording that is part of the CSP W3C specification,
these terms are not "invented" by TYPO3. Since reading the W3C RFC can be very
intimidating, here are a few key concepts.
Note
Skip to the section Example scenario to see a "real-life" usage
scenario, if you can better understand from actual code examples.
Directives
CSP consists of multiple rules (or "directives"), that are part of a "policy". This
policy says, what functionality the site's output is allowed to use.
With that, several HTML tags can be controlled, like from which URLs images can be
requested from, if and from where iframes are allowed, if and from where JavaScripts
are allowed and so on. There is a long list of applicable directives, see
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives
with specific identifiers like default-src, img-src and so on.
Each directive may have several "attributes" (like the allowed URLs).
Directives may build upon each other, a bit like CSS definitions (Cascading Style Sheet)
do. However, these are more meant to modify a basic rule on an earlier level,
and called "mutations". The relation of a "child rule" to its "parent" is
also called "ancestor chain". An example:
If frame-src is not defined, it falls back to child-src, and finally falls
back to default-src. But if frame-src is defined, it is used, and
the sources from default-src are not used. In such a case, default-src listed
sources have to be repeated (when wanted) explicitly in frame-src.
Applying the policy
A final policy is compiled of all these directives, and then sent as a HTTP
response header Content-Security-Policy: ... (respectively
Content-Security-Policy-Reporty-Only).
In TYPO3, directives can be specified via PHP syntax (within Extensions) and
YAML syntax (within site configuration). Additionally, rules can be
set via the PSR-14 event PolicyMutatedEvent.
Mutations
These rules can influence each other, this is where the concept of "mutations" come in.
The "policy builder" of TYPO3 applies each configured mutation, no matter where it was
defined.
Because of this, each mutation (directive definition) needs a specific "mode" that can
instruct, how this mutation is applied: Should an existing directive be
set, inherited, appended, remove or extended to the final policy (see
Content Security Police modes).
Each directive is then applied in regard to its defined mode and can list one or more
"sources" with the values of additional parameters of a directive. Sources are
web site addresses / URLs (or just protocols), and also include some special keywords like
self/none/data:.
Nonces
There are possible exemptions to directives for specific content created on specific
pages created by TYPO3 in your frontend (or backend modules). To verify, that these
exemptions are valid in a policy, a so-called "Nonce" (a unique "number used once")
is created (details on https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce).
TYPO3 can manage these Nonces and apply them were configured.
Nonces are retrieved from
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce
and will be used for any directive within the scope of a single HTTP request.
When a webpage with activated policies is shown in a client's browser, each HTML tag
violating the policy will not be interpreted by the browser.
Depending on a configuration of a possible "Report", such violations can be submitted
back to a server and be evaluated there. TYPO3 provides such an endpoint to receive
and display reports in a backend module, but also third-party servers are usable.
Policies can be declared with "dispositions", to indicate how they are handled.
"Enforce" means that a policy is in effect, and "Report" allows to only pretend
a policy is in effect, to gather knowledge about possible improvements of a
webpage's output. Both dispositions can be set independently in TYPO3.
You have one TYPO3 installation with two sites (frontend), example.com and example.org.
You have created custom backend modules for some distinct functionality.
example.com is a site where your editors fully control all frontend content, and they
want to have full flexibility of what and how to embed. You use a CDN network to deliver
your own large assets, like videos.
example.org is a community-driven site, where users can manage profiles and post chats,
and where you want to prevent exploits on your site. Some embedding to a set of allowed
web services (YouTube, Google Analytics) must be possible.
So you need to take care of security measures, and find a pragmatic way how to allow foreign
content (like YouTube, widgets, tracking codes, assets)
Specifically you want to to set these following rules, as an example.
Note
The domains listed are for demonstration only, and will not match real requirements;
for example, YouTube is already allowed by the default TYPO3 frontend CSP configuration,
which can be inherited.
Rules for example.com (editorial)
<iframe> to many services should be allowed
<img> sources to anywhere should be allowed
<script> sources to cdn.example.com and *.youtube.com and *.google.com
should be allowed
Rules for example.org (community)
<iframe> to cdn.example.com, *.youtube.com should be allowed
<img> sources to cdn.example.com and *.instagram.com should be allowed
<script> sources to cdn.example.com and *.youtube.com and *.google.com
should be allowed
Rules for the TYPO3 backend
Normal TYPO3 backend rules need to be applied, so we only want to add some
rules for custom backend modules:
<iframe> to cdn.example.com should be allowed
<img> sources to cdn.example.com should be allowed
<script> sources to cdn.example.com should be allowed
Resulting configuration example:
And this is how you would do that with a CSP YAML configuration file, one per site:
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)inheritDefault:truemutations:# Allow frames/iframes to TRUSTED specific locations# Avoid "protocol only" white-list like "https:" here,# because it could inject javascript easily, the most important reason# why CSP was invented was to block security issues like this.# (Note: it's "frame-src" not "iframe-src")-mode:"extend"directive:"frame-src"sources:-"https://*.example.org"-"https://*.example.com"-"https://*.instagram.com"-"https://*.vimeo.com"-"https://*.youtube.com"# Allow img src to anyhwere (HTTPS only, not HTTP)-mode:"extend"directive:"img-src"sources:-"https:"# Allow script src to the specified domains (HTTPS only)-mode:"extend"directive:"script-src"sources:-"https://cdn.example.com"-"https://*.youtube.com"-"https://*.google.com"
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)inheritDefault:truemutations:# Allow frame/iframe src to the specified domains (HTTPS only)-mode:"extend"# (Note: it's "frame-src" not "iframe-src")directive:"frame-src"sources:-"https://cdn.example.com"-"https://*.youtube.com"# Allow img src to the specified domains (HTTPS only)-mode:"extend"directive:"img-src"sources:-"https://cdn.example.com"-"https://*.instagram.com"# Allow script src to the specified domains (HTTPS only)-mode:"extend"directive:"script-src"sources:-"https://cdn.example.com"-"https://*.youtube.com"-"https://*.google.com"
This is really just a simple demo, that has room for improvements. For example,
the allowed list of *-src values to any directive could actually be set through their common
parent, the default-src attribute. There is a very deep and nested possibility
to address the attributes of many HTML5 tags, which is covered in depth on
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives.
You can take a look into the PHP enum
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive ,
which gives you an overview of all supported directives.
Read on to understand more of the underlying API builder concepts below.
Configuration
Policy builder approach
The following approach illustrates how a policy is build:
<?phpuseTYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
useTYPO3\CMS\Core\Security\Nonce;
$nonce = Nonce::create();
$policy = (new Policy())
// Results in `default-src 'self'`
->default(SourceKeyword::self)
// Extends the ancestor directive ('default-src'),// thus reuses 'self' and adds additional sources// Results in `img-src 'self' data: https://*.typo3.org`
->extend(Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org'))
// Extends the ancestor directive ('default-src'),// thus reuses 'self' and adds additional sources// Results in `script-src 'self' 'nonce-[random]'`// ('nonce-proxy' is substituted when compiling the policy)
->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)
// Sets (overrides) the directive,// thus ignores 'self' of the 'default-src' directive// Results in `worker-src blob:`
->set(Directive::WorkerSrc, SourceScheme::blob);
header('Content-Security-Policy: ' . $policy->compile($nonce));
Copied!
The result of the compiled and serialized result as HTTP header would look
similar to this (the following sections are using the same example, but utilize
different techniques for the declarations):
The policy builder is the low-level representation and interaction in PHP,
any other configuration is using the same verbs to describe the CSP
instructions. The
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy
object is used for compiling the CSP in a middleware.
Thus, custom controllers or middlewares could use this approach; the last
line
<?phpdeclare(strict_types=1);
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
useTYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
useTYPO3\CMS\Core\Type\Map;
return Map::fromEntries(
[
// Provide declarations for the backend
Scope::backend(),
// NOTICE: When using `MutationMode::Set` existing declarations will be overriddennew MutationCollection(
// Results in `default-src 'self'`new Mutation(
MutationMode::Set,
Directive::DefaultSrc,
SourceKeyword::self,
),
// Extends the ancestor directive ('default-src'),// thus reuses 'self' and adds additional sources// Results in `img-src 'self' data: https://*.typo3.org`new Mutation(
MutationMode::Extend,
Directive::ImgSrc,
SourceScheme::data,
new UriValue('https://*.typo3.org'),
),
// NOTICE: the following two instructions for `Directive::ImgSrc` are identical to the previous instruction,// `MutationMode::Extend` is a shortcut for `MutationMode::InheritOnce` and `MutationMode::Append`// new Mutation(MutationMode::InheritOnce, Directive::ImgSrc, SourceScheme::data),// new Mutation(MutationMode::Append, Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org')),// Extends the ancestor directive ('default-src'),// thus reuses 'self' and adds additional sources// Results in `script-src 'self' 'nonce-[random]'`// ('nonce-proxy' is substituted when compiling the policy)new Mutation(
MutationMode::Extend,
Directive::ScriptSrc,
SourceKeyword::nonceProxy,
),
// Sets (overrides) the directive,// thus ignores 'self' of the 'default-src' directive// Results in `worker-src blob:`new Mutation(
MutationMode::Set,
Directive::WorkerSrc,
SourceScheme::blob,
),
),
],
[
// You can also additionally provide frontend declarations
Scope::frontend(),
new MutationCollection(
// Sets (overrides) the directive,// thus ignores 'self' of the 'default-src' directive// Results in `worker-src https://*.workers.example.com:`new Mutation(
MutationMode::Set,
Directive::WorkerSrc,
new UriValue('https://*.workers.example.com'),
),
),
],
);
Copied!
The API here is much like the YAML syntax. The PHP code needs to return
a mapped array of an
MutationCollection instance with all rules
put into a sub-array, containing instances of a single
Mutation.
Each
Mutation instance is like a Data Object (DO) where its constructor
allows you to specifiy a mode (type
MutationMode), a directive
(type
Directive) and one ore more actual values ("sources", type
UriValue
or SourceKeyword).
Additionally, a
Scope instance object is included, which can either
be
Scope::backend() or
Scope::frontend().
A good PHP IDE will allow for good autocompletion and hinting, and using
a boilerplate configuration like the example above helps you to get started.
Backend-specific
The YAML configuration only applies to the frontend part of TYPO3.
Backend policies need to be set using the PHP API, within an extension
as described in the section above.
You need to ensure that Scope::backend() is set in the mapped return array
for the rules you want to setup.
Site-specific (frontend)
In frontend, a dedicated sites/<my_site>/csp.yaml can be
used to declare policies for a specific site, for example:
# Inherits default site-unspecific frontend policy mutations (enabled per default)inheritDefault:truemutations:# Results in `default-src 'self'`-mode:"set"directive:"default-src"sources:-"'self'"# Extends the ancestor directive ('default-src'),# thus reuses 'self' and adds additional sources# Results in `img-src 'self' data: https://*.typo3.org`-mode:"extend"directive:"img-src"sources:-"data:"-"https://*.typo3.org"# Extends the ancestor directive ('default-src'),# thus reuses 'self' and adds additional sources# Results in `script-src 'self' 'nonce-[random]'`# ('nonce-proxy' is substituted when compiling the policy)-mode:"extend"directive:"script-src"sources:-"'nonce-proxy'"# Results in `worker-src blob:`-mode:"set"directive:"worker-src"sources:-"blob:"
Copied!
Disable CSP for a site
The Content Security Policy for a particular site can be disabled with the
active key set to
false:
# "active" is enabled by default if omittedactive:false
Copied!
Site-specific Content-Security-Policy endpoints
The reporting endpoint is used to receive browser reports about violations to
the security policy, for example if a YouTube URL was requested, but could
not be displayed in an iframe due to a directive not allowing this.
Reports like this can help to gain insight, what URLs are used by editors
and might need inclusion into the policy.
Since reports can be sent by any browser, they can possibly easily flood
a site with requests and take up storage space. Reports are stored in the
sys_http_report database table when using the endpoint provided by TYPO3.
To influence whether this endpoint accepts reports,
the disposition-specific property reportingUrl can be configured and
set to either:
true
to enable the reporting endpoint
false
to disable the reporting endpoint
(string)
to use the given value as external reporting endpoint
The nonce attribute is useful to allowlist specific elements, such as a
particular inline script or style elements. It can help you to avoid using
the CSP unsafe-inline directive, which would allowlist all inline scripts or
styles.
<linkrel="stylesheet"href="/_assets/af46f1853e4e259cbb8ebcb816eb0403/Css/styles.css?1687696548"media="all"nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
><stylenonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">/* some inline styles */</style><scriptsrc="/_assets/27334a649e36d0032b969fa8830590c2/JavaScript/scripts.js?1684880443"nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
></script><scriptnonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">/* some inline JavaScript */</script>
Copied!
The nonce changes with each request so that (possibly malicious) inline scripts
or styles are blocked by the browser.
The nonce is applied automatically, when scripts or styles are defined with the
TYPO3 API, like TypoScript (
page.includeJS, etc.) or the
asset collector. This only refers to referenced files
(via
src and
href attributes) and not inline scripts
or inline styles. For those, you should either use the PHP/Fluid approach
as listed below, or use TypoScript only for passing DOM attributes
and using external scripts to actually evaluate these attributes to control
functionality.
TYPO3 provides APIs to get the nonce for the current request:
Nonces are implemented via a PSR middleware and thus applied dynamically. This
also means, they are somewhat "bad" for caching (especially for reverse proxies),
since they create unique output for a specific visitor.
Since the goal of nonces are to allow "exemptions" for otherwise forbidden content,
this closely relates to validity or integrity of this forbidden content. Instead
of emitting unique nonces, another possibility is to utilize hashing functionality
to content regarded as "safe".
This can be done with sha256/sha384/sha512 hashing of referenced script, and including
them as a valid directive, like this:
The "sha256-..." block would be the SHA256 hash created from a file like 'script.js'.
For example, a file like this:
script.js (some javascript file that is included in your website)
console.log('Hello.');
Copied!
would correspond to a SHA256 hash of 6c7d3c1bf856597a2c8ae2ca7498cb4454a32286670b20cf36202fa578b491a9.
Note
These hashes can be created by shell scripts like sha256 and several libraries,
also in nodeJS bundling tools.
The browser would evaluate a reference JavaScript file and calculate it's SHA256
hash and compare it to the list of allowed hashes.
The downside of this is: Everytime an embedded file changes (like via build processes),
the CSP SHA hash would need to be adopted. This could be automated by a PHP definition
of CSP rules and hashing files automatically, which would be a performance-intense
process and call for its own caching.
There is no automatism for this kind of hashing in TYPO3 (yet, see
https://forge.typo3.org/issues/100887), so it has to be done manually
as outlined above.
Reporting of violations, CSP Backend module
Potential CSP violations are reported back to the TYPO3 system and persisted
internally in the database table
sys_http_report. A corresponding
Admin Tools > Content Security Policy backend module supports users
to keep track of recent violations and - if applicable - to select potential
resolutions (stored in the database table
sys_csp_resolution) which
extends the Content Security Policy for the given scope during runtime:
Backend module "Content Security Policy" which displays the violations
Clicking on a row displays the details of this violation on the right side
including suggestions on how to resolve this violation. You have the choice to
apply this suggestion, or to mute or delete the specific violation.
Note
If you apply the suggestion, it is stored in the database table
sys_csp_resolution. To have all policies in one place, you
should consider adding the suggestion to your
extension-specific or
site-specific CSP definitions
manually.
Warning
Resolutions, once applied, can not be removed again via the GUI. You would
need to manually remove entries in the
sys_csp_resolution database
table.
Using a third-party service
As an alternative to the built-in reporting module, an external reporting URL
can be configured to use a third-party service as well:
config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
= 'https://csp-violation.example.org/';
// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
= 'https://csp-violation.example.org/';
Copied!
Violations are then sent to the third-party service instead of the TYPO3
endpoint. Resolutions would then not be applied dynamically.
If defined, the site-specific configuration takes precedence over
the global configuration.
In case the explicitly disabled endpoint still would be called, the
server-side process responds with a 403 HTTP error message.
The global scope-specific setting contentSecurityPolicyReportingUrl can
be set to zero ('0') to disable the CSP reporting endpoint:
config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';
// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!
Active content security policy rules
The backend module System > Configuration > Content Security Policy Mutations
uses a simple tree display of all configured directives, grouped by
frontend or backend. Each rule shows where it is defined, and what
its final policy is set to:
Backend module "Configuration > Content Security Policy Mutations" which displays
a tree of all policy directives.
The Context API encapsulates various information for data retrieval (for
example, inside the database) and analysis of current permissions and caching
information.
The context is set up at the very beginning of each TYPO3 entry point, keeping
track of, for example, the current time, if a user is logged in and which
workspace is currently accessed.
The
\TYPO3\CMS\Core\Context\Context object can be retrieved via
dependency injection:
This property is kept for compatibility reasons. Do not use, if not really
necessary, the option will be removed rather sooner than later.
Overlay types
LanguageAspect::OVERLAYS_OFF
Just fetch records from the selected language as given by
LanguageAspect->getContentId(). No overlay will happen, no
fetching of the records from the default language. This boils down to
"free mode" language handling. Records without a default language parent are
included.
LanguageAspect::OVERLAYS_MIXED
Fetch records from the default language and overlay them with translations.
If a record is not translated, the default language will be used.
LanguageAspect::OVERLAYS_ON
Fetch records from the default language and overlay them with translations.
If a record is not translated, it will not be displayed.
LanguageAspect::OVERLAYS_ON_WITH_FLOATING
Fetch records from the default language and overlay them with translations.
If a record is not translated, it will not be shown. Records without a
default language parent are included.
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:
TYPO3 ships a list of countries of the world. The list is based on the
ISO 3166-1 standard, with the alphanumeric short name ("FR" or "FRA" in its
three-letter short name), the English name ("France"), the official name
("Republic of France"), also the numerical code, and the country's flag as
emoji (UTF-8 representation).
Note
The country list is based on Debian's ISO code list and shipped
statically as PHP content in the Country API.
The method returns an array of
\TYPO3\CMS\Core\Country\Country objects.
Get a country
EXT:my_extension/Classes/MyClass.php
// Get the country by Alpha-2 code
$france = $this->countryProvider->getByIsoCode('FR');
// Get the country by name
$france = $this->countryProvider->getByEnglishName('France');
// Get the country by Alpha-3 code
$france = $this->countryProvider->getByAlpha3IsoCode('FRA');
Copied!
The methods return a
\TYPO3\CMS\Core\Country\Country object.
Filter countries
One can use filters to get the desired countries:
EXT:my_extension/Classes/MyClass.php
useTYPO3\CMS\Core\Country\CountryFilter;
$filter = new CountryFilter();
// Alpha-2 and Alpha-3 ISO codes can be used
$filter
->setOnlyCountries(['AT', 'DE', 'FR', 'DK'])
->setExcludeCountries(['AUT', 'DK']);
// Will be an array with "Germany" and "France"
$filteredCountries = $this->countryProvider->getFiltered($filter);
Copied!
The method
getFiltered() return an array of
\TYPO3\CMS\Core\Country\Country objects.
The
Country object
A country object can be used to fetch all information about it, also with
translatable labels:
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)
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.
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.
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.
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:
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:
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.
Used internally by TYPO3 and not accessible in the backend (e.g. tables such as
be_sessions,
sys_registry, cache tables). They are accessed via TYPO3
APIs such as the caching framework. These tables are not
editable unless a specific backend module provides access.
Typical categories include:
Cache tables: Created automatically when using a database-based cache
backend
Session tables:
fe_sessions,
be_sessions
System tables:
sys_registry: Global configuration
sys_log: Viewable via System > Log
Managed tables
Defined in the TCA and, by default, editable in the
Web > List module. TYPO3 derives database schemas from the TCA
configuration. Required fields such as
uid and
pid are generated
automatically.
When records are rendered in the backend using the
FormEngine, entries
with the soft delete flag set (
deleted) will not be shown.
When querying tables via TypoScript, visibility fields such as
hidden,
startdate, and
enddate are respected.
If you use the
DBAL query builder to access
the database, the
restriction builder
automatically filters records based on visibility fields unless explicitly disabled.
When using an
Extbase repository, the
query settings
also apply visibility constraints by default, but can be reconfigured to change
this behavior.
The
pages table
Defines TYPO3's hierarchical page tree. All managed records reference a
pages.uid via their
pid.
The root page has pid = 0 and does not exist as a row in the table.
Only administrators can create records on the root level.
Tables must explicitly allow root-level records using
-.
Many-to-many (MM) relations
MM tables store relationships between records. Examples include:
sys_category_record_mm: Categories and categorized records
sys_file_reference: File usage in content and pages
These tables may appear in the backend if configured via
inline records.
Database records
In TYPO3, a record refers to an individual piece of content or data that
is stored in the database. Each record is part of a table and represents a
specific entity, such as a page, a content element, a backend user, or an
extension configuration.
TYPO3 uses a modular structure where different types of data are managed as
records, making it easy to organize and manipulate content.
Understanding records in TYPO3 is fundamental, as they are the building blocks
for managing content and data within the system.
If you are using the
stefanfroemken/ext-kickstarter
you can use
command vendor/bin/typo3 make:table to create the TCA definition for a
table where database records can be stored in.
Common examples of records in TYPO3:
Page records
These represent pages in the page tree, which structure the website. They
are stored in table
pages.
Content records
Every content record consists of sub entities like text, images,
videos, and so on. Content records can be placed on a page. They are stored
in table
tt_content. TYPO3 has some pre configured content elements
like for example Header Only, Regular Text Element, Text & Images,
and Images Only.
Backend user records
The user records consist of information about the users who have access to
the TYPO3 backend. They are stored in table
be_users. Users are
organized in user groups which are stored in table
be_groups.
System records
System records control the configuration and management of the TYPO3 system.
Examples include file references, file mounts, or categories. For example,
you can create a category and assign it to some content records in order to
indicate that they belong together.
Extension-specific records
Extensions often define custom records to store specific data, such as
products for a shop system or events for a calendar.
Technical structure of a record:
Each record is stored in a database table. Each row represents one record. Each
column represents a field of the record or some kind of metadata.
A record typically includes a unique identifier in column uid, the id of the
page record on which it is located in column pid, columns for various
attributes (for example, title, content), and metadata like creation and
modification timestamps, visibility, information on translation
and workspace handling. A record can have relations to other records.
TCA (Table Configuration Array)
TYPO3 uses the TCA to define how records of a specific table are
structured, how they are displayed in the backend, and how they interact
with other parts of the system. See the TCA Reference
for details.
Types and subtypes in records
In TYPO3, different types and subtypes of records are often stored in the same
database table, even though not all types share the same columns. This approach
allows for flexibility and efficiency in handling diverse content and data
structures within a unified system.
TYPO3 uses a single-table inheritance strategy, where records of various
types are distinguished by a specific field, often named
type.
For historical reasons the field is named
CType for content elements
and
doktype for pages. The field that is used for the type is defined in
TCA in ctrl > type. The types itself are stored in
types.
This allows TYPO3 to store related records, such as different content types,
in a shared table like
tt_content while supporting custom
fields for each record type.
For content elements in table
tt_content there is a second level of
subtypes in use where the field CType contains the value "list" and the field
list-type contains the actual type. This second level of types exists for
historic reasons. Read more about it in chapter Content Elements & Plugins.
Record objects
New in version 13.2
Record objects have been introduced as an experimental feature.
Record objects are instances of
\TYPO3\CMS\Core\Domain\Record and
contain an object-oriented representation of a database record.
A record object can be used to output a database record in Fluid
when no Extbase domain model is available.
TYPO3 extensions based on Extbase typically introduce a class inheriting from
\TYPO3\CMS\Extbase\DomainObject\AbstractEntity to represent a record
fully or partially within the domain of the extension. Multiple Extbase models
can store their data in the same database table. Additionally, the same record
can be represented in various ways by different Extbase models, depending on
the specific requirements of each model.
Record objects have been introduced as an experimental feature.
Record objects are instances of
\TYPO3\CMS\Core\Domain\Record .
They are an advanced data object holding the data of a database row, taking the
TCA definition and possible relations of that database row
into account.
Note
The Record object is available but still considered experimental. Only the
usage in Fluid is public API.
In PHP a record object can be created by the
\TYPO3\CMS\Core\Domain\RecordFactory .
The event RecordCreationEvent can be used to influence or replace
the Record object and its properties during creation.
Use records in Fluid
In frontend templates the record object is provided by
TypoScript or passed to Fluid by a
PHP class.
Content element preview templates automatically receive a record object
representing the record of the content element that should currently be displayed.
The Debug ViewHelper <f:debug> output
of the Record object is misleading for integrators,
as most properties are accessed differently as one would assume.
We are dealing with an object here. You however can access your record
properties as you are used to with
{record.title} or
{record.uid}. In addition, you gain special, context-aware properties
like the language
{record.languageId} or workspace
{data.versionInfo.workspaceId}.
Overview of all possibilities:
Demonstration of available variables in Fluid
<!-- Any property, which is available in the Record (like normal) -->
{record.title}
{record.uid}
{record.pid}
<!-- Language related properties -->
{record.languageId}
{record.languageInfo.translationParent}
{record.languageInfo.translationSource}
<!-- The overlaid uid -->
{record.overlaidUid}
<!-- Types are a combination of the table name and the Content Type name. --><!-- Example for table "tt_content" and CType "textpic": --><!-- "tt_content" (this is basically the table name) -->
{record.mainType}
<!-- "textpic" (this is the CType) -->
{record.recordType}
<!-- "tt_content.textpic" (Combination of mainType and record type, separated by a dot) -->
{record.fullType}
<!-- System related properties -->
{record.systemProperties.isDeleted}
{record.systemProperties.isDisabled}
{record.systemProperties.isLockedForEditing}
{record.systemProperties.createdAt}
{record.systemProperties.lastUpdatedAt}
{record.systemProperties.publishAt}
{record.systemProperties.publishUntil}
{record.systemProperties.userGroupRestriction}
{record.systemProperties.sorting}
{record.systemProperties.description}
<!-- Computed properties depending on the request context -->
{record.computedProperties.versionedUid}
{record.computedProperties.localizedUid}
{record.computedProperties.requestedOverlayLanguageId}
{record.computedProperties.translationSource} <!-- Only for pages, contains the Page model --><!-- Workspace related properties -->
{record.versionInfo.workspaceId}
{record.versionInfo.liveId}
{record.versionInfo.state.name}
{record.versionInfo.state.value}
{record.versionInfo.stageId}
Copied!
Using the raw record
The
RecordFactory object contains
only the properties, relevant for
the current record type, for example CType.
In case you need to access properties, which are not defined for the record
type, the "raw" record can be used by accessing it via
{record.rawRecord}. Those properties are not transformed.
Database (Doctrine DBAL)
This chapter describes accessing the database on the level of the Doctrine
Database Abstraction Layer (DBAL).
The Doctrine Database Abstraction Layer (DBAL) in TYPO3 provides developers
with a powerful and flexible way to interact a database, allowing them to
perform database operations through an object-oriented API while ensuring
compatibility across different database systems.
In Extbase extensions tables are abstracted as
Extbase models. Operations such as creating, updating and
deleting database records are usually performed from within a
Extbase repository with methods provided by Extbase
classes. However, Doctrine DBAL can also be used by extensions that use, for
example, an Extbase controller.
TYPO3 relies on storing its data in a relational database management
system (RDBMS). The Doctrine DBAL component is used to enable connecting to
different database management systems. Most used is still MySQL / MariaDB, but
thanks to Doctrine others like PostgreSQL and SQLite are also an option.
The corresponding DBMS can be selected during installation.
This chapter gives an overview of the basic TYPO3 database table structure,
followed by some information on upgrading and maintaining table and field
consistency, and then deep dives into the programming API.
Doctrine DBAL
Database queries in TYPO3 are done with an API based on Doctrine DBAL.
The API is provided by the system extension core, which is always loaded and
thus always available.
Extension authors can use this low-level API to manage query operations
directly on the configured DBMS.
Doctrine DBAL is rich in features. Drivers for various target systems enable
TYPO3 to run on a long list of ANSI SQL-compatible DBMSes. If used properly,
queries created with this API are translated to the specific database engine by
Doctrine without an extension developer taking care of that specifically.
The API provided by the Core is basically a pretty small and lightweight facade
in front of Doctrine DBAL that adds some convenient methods as well as some
TYPO3-specific sugar. The facade additionally provides methods to retrieve
specific connection objects per configured database connection based on the
table that is queried. This enables instance administrators to configure
different database engines for different tables, while being transparent to
extension developers.
This document does not outline every single method that the API provides. It
sticks to those that are commonly used in extensions, and some parts like the
rewritten schema migrator are omitted as they are usually of little to no
interest to extensions.
Understanding Doctrine DBAL and Doctrine ORM
Doctrine is a two-part project, with Doctrine DBAL being the low-level
database abstraction and the interface for building queries to specific database
engines, while Doctrine ORM is a high-level object relational mapping on top
of Doctrine DBAL.
The TYPO3 Core implements - only - the DBAL part. Doctrine ORM is neither
required nor implemented nor used.
Low-level and high-level database calls
This documentation focuses on low-level database calls. In many cases, it is
better to use higher level APIs such as the DataHandler or Extbase repositories and
to let the framework handle persistence details internally.
Tip
Always remember the high-level database calls and use them when
appropriate!
Configuration
The configuration of Doctrine DBAL for TYPO3 is about specifying the single
database endpoints and passing the connection credentials. The framework
supports the parallel usage of multiple database connections, a specific
connection is mapped depending on its table name. The table space can be seen as
a transparent layer that determines which specific connection is chosen for a
query to a single or a group of tables: It allows "swapping out" single tables
from the Default connection to point them to a different database endpoint.
As with other central configuration options, the database endpoint and mapping
configuration is done in config/system/settings.php and ends up in
$GLOBALS['TYPO3_CONF_VARS'] after the Core bootstrap. The specific sub-array is
$GLOBALS['TYPO3_CONF_VARS']['DB'] .
Example: one connection
A typical basic example using only the Default connection with a single
database endpoint:
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
Attention
Changed in version 13.0
TYPO3 expects all "main" Core system tables to be configured for the
Default connection (especially
sys_*,
pages,
tt_content and in general all tables that have
TCA configured). The reason for this is to improve
performance with joins between tables. Cross-database joins are almost
impossible.
One scenario for using a separate database connection is to query data
directly from a third-party application in a custom extension. Another
use case is database-based caches.
Another example with two connections, where the
be_sessions table is
mapped to a different endpoint:
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.
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!
Note
The default restrictions
deleted,
hidden,
startime and
endtime based on the TCA setting of a table are
only applied to
select() calls, they are not added for
delete() or other query types.
Select multiple rows with some "where" magic
Advanced query using the
QueryBuilder and manipulating the default
restrictions:
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.
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:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'tx_myextension_domain_model_mytable';
publicfunction__construct(
private readonly ConnectionPool $connectionPool,
){}
publicfunctionfindSomething(){
// Get a query builder for a table
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
// Or get a connection for a table
$connection = $this->connectionPool
->getConnectionForTable(self::TABLE_NAME);
}
}
Copied!
The
QueryBuilder is the default object used by extension
authors to express complex queries, while a
Connection instance can be
used as a shortcut to handle some simple query cases.
Pooling: multiple connections to different database endpoints
TYPO3 can handle multiple connections to different database endpoints at the
same time. This can be configured for each individual table in
$GLOBALS['TYPO3_CONF_VARS'] (see database configuration for details). This makes it possible to run tables on
different databases without an extension developer having to worry about it.
The
ConnectionPool
implements this feature: It looks for configured
table-to-database mapping and can return a
Connection or a
QueryBuilder instance
for that specific connection. These objects know
internally which target connection they are dealing with and will quote field
names accordingly, for instance.
Beware
However, the transparency of tables for different database endpoints is limited.
Executing a table
JOIN between two tables that reference different
connections will result in an exception. This restriction may in practice lead
to implicit "groups" of tables that must to point to a single connection when an
extension or the TYPO3 Core joins these tables.
This can be problematic when several different extensions use, for instance, the
Core category or collection API with their mm table joins between Core internal
tables and their extension counterparts.
That situation is not easy to deal with. At the time of writing the Core
development will implement eventually some non-join fallbacks for typical cases
that would be good to decouple, though.
Tip
In the case joins cannot be decoupled, but still need to run affected tables
on different databases, and when the code can not be easily adapted, some
DBMS like PostgreSQL allow these queries to be executed by having their own
connection handlers to various other endpoints.
The query builder provides a set of methods to create queries
programmatically.
This chapter provides examples of the most common queries.
Warning
From a security point of view, the documentation of
->createNamedParameter()
and ->quoteIdentifier() are
an absolute must read and follow section. Make very sure you understand
this and use it for each and every query to prevent SQL
injections!
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.
Never instantiate and initialize the query builder manually using
dependency injection or
GeneralUtility::makeInstance(), otherwise you
will miss essential dependencies and runtime setup.
Warning
The QueryBuilder holds internal state and must not be reused for
different queries. In addition, a reuse comes with a
significant performance penalty and memory consumption.
Use one query builder per query. Get a fresh one by calling
$connection->createQueryBuilder() if the same table is
involved, or use
$connectionPool->getQueryBuilderForTable() for a
query to a different table. Do not worry, creating those object instances
is quite fast.
->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:
// 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:
$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:
->select() and
->count() queries trigger TYPO3 magic that adds
further default where clauses if the queried table is also registered via
$GLOBALS['TCA'] . See the RestrictionBuilder section for details on that topic.
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:
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:
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() 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:
->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:
->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:
// 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():
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:
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:
->join() and innerJoin are identical. They create an
INNER JOIN query, this is identical to a
JOIN query.
->leftJoin() creates a
LEFT JOIN query, this is identical to
a
LEFT OUTER JOIN query.
->rightJoin() creates a
RIGHT JOIN query, this is identical to
a
RIGHT OUTER JOIN query.
Calls to
join() methods are only considered for
->select() and
->count() type queries.
->delete(),
->insert()
and
update() do not support joins, these query parts are ignored and
do not end up in the final statement.
The argument of
->getQueryBuilderForTable() should be the leftmost
main table.
Joining two tables that are configured to different connections will throw
an exception. This restricts the tables that can be configured for different
database endpoints. It is possible to test the connection objects of the
involved tables for equality and implement a fallback logic in PHP if they
are different.
Doctrine DBAL does not support the use of join methods in combination with
->update(),
->insert() and
->delete() methods, because
such a statement is not cross-platform compatible.
Multiple join condition expressions can be resolved as strings like:
->orderBy() resets all previously specified orders. It makes no sense
to call this function again after a previous
->orderBy() or
->addOrderBy().
Both methods need a field name or a
table.fieldName or a
tableAlias.fieldName as first argument. In the example above the call
to
->orderBy('sys_language.sorting') would have been identical. All
identifiers are quoted automatically.
The second, optional argument of both methods specifies the sort order. The
two allowed values are
'ASC' and
'DESC', where
'ASC'
is default and can be omitted.
To create a chain of orders, use
->orderBy() and then multiple
->addOrderBy() calls. The call to
->orderBy('header')->addOrderBy('bodytext')->addOrderBy('uid', 'DESC')
creates ORDER BY `header` ASC, `bodytext` ASC, `uid` DESC
To achieve more complex sortings, which can't be created with QueryBuilder,
you can fall back on the underlying raw Doctrine QueryBuilder. This is
accessible with
->getConcreteQueryBuilder(). It doesn't do any
quoting, so you can do something like
$concreteQueryBuilder->orderBy('FIELD(eventtype, 0, 4, 1, 2, 3)');.
Make sure to quote properly as this is entirely your responsibility with the
Doctrine QueryBuilder!
groupBy() and addGroupBy()
Add
GROUP BY to a
->select() statement. Each argument of the
methods is a single identifier:
// GROUP BY `pages`.`sys_language_uid`, `sys_language`.`uid`
->groupBy('pages.sys_language_uid', 'sys_language.uid');
Copied!
Remarks:
Similar to
->select() and
->where(), both methods are variadic
and take any number of arguments, argument unpacking is supported:
->groupBy(...$myGroupArray)
Each argument is either a direct field name GROUP BY `bodytext`,
a
table.fieldName or a
tableAlias.fieldName and is properly
quoted.
->groupBy() resets all previously defined group specification and
should only be called once per statement.
For more complex statements you can use the raw Doctrine QueryBuilder.
See remarks for orderBy()
union() and addUnion()
Method union() provides a streamlined way to combine result sets from multiple
queries.
union(string|QueryBuilder $part)
Creates the initial
UNION query part by accepting either a raw SQL
string or a QueryBuilder instance. Calling union() resets all previous
union definitions, it should therefore only be called once, using addUnion()
to add subsequent union parts.
Adds additional
UNION parts to the query. The $type parameter accepts:
UnionType::DISTINCT
Combines results while eliminating duplicates.
UnionType::ALL
Combines results and retains all duplicates. Not removing duplicates can
be a performance improvement.
Note
While technically possible, it is not recommended to send direct SQL queries
as strings to the union() and addUnion() methods. We recommend to use a
query builder.
If you decide to do so you must take care of quoting, escaping, and
valid SQL Syntax for the database system in question. The Default Restrictions
are not applied on that part.
Named placeholders, such as created by
QueryBuilder::createNamedParameter()must be created on the outer most QueryBuilder See the example below.
Database provider support of union() and addUnion()
QueryBuilder can be used create
UNION clause queries not compatible with all database providers,
for example using
LIMIT/OFFSET in each part query or other stuff.
When building functional tests, run them on all database types that should
be supported.
<?phpnamespaceMyExtension\MyVendor\Service;
useDoctrine\DBAL\Query\UnionType;
useTYPO3\CMS\Core\Database\Connection;
useTYPO3\CMS\Core\Database\ConnectionPool;
useTYPO3\CMS\Core\Database\Query\QueryBuilder;
final readonly classMyService{
publicfunction__construct(
private ConnectionPool $connectionPool,
){}
publicfunctiongetTitlesOfSubpagesAndContent(
int $parentId,
): ?array{
$connection = $this->connectionPool->getConnectionForTable('pages');
$unionQueryBuilder = $connection->createQueryBuilder();
// Passing the outermost QueryBuilder to the subqueries
$firstPartQueryBuilder = $this->getUnionPart1QueryBuilder($connection, $unionQueryBuilder, $parentId);
$secondPartQueryBuilder = $this->getUnionPart2QueryBuilder($connection, $unionQueryBuilder, $parentId);
return $unionQueryBuilder
->union($firstPartQueryBuilder)
->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
privatefunctiongetUnionPart1QueryBuilder(
Connection $connection,
QueryBuilder $unionQueryBuilder,
int $pageId,
): QueryBuilder{
$queryBuilder = $connection->createQueryBuilder();
// The union Expression Builder **must** be used on subqueries
$unionExpr = $unionQueryBuilder->expr();
$queryBuilder
// The column names of the first query are used// The column count of both subqueries must be the same// The data types must be compatible across columns of the queries
->select('title', 'subtitle')
->from('pages')
->where(
// The union Expression Builder **must** be used on subqueries
$unionExpr->eq(
'pages.pid',
// Named parameters **must** be created on the outermost (union) query builder
$unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
),
);
return $queryBuilder;
}
privatefunctiongetUnionPart2QueryBuilder(
Connection $connection,
QueryBuilder $unionQueryBuilder,
int $pageId,
): QueryBuilder{
$queryBuilder = $connection->createQueryBuilder();
// The union Expression Builder **must** be used on subqueries
$unionExpr = $unionQueryBuilder->expr();
$queryBuilder
// The column count of both subqueries must be the same
->select('header', 'subheader')
->from('tt_content')
->where(
$unionExpr->eq(
'tt_content.pid',
// Named parameters **must** be created on the outermost (union) query builder
$unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
),
);
return $queryBuilder;
}
}
Copied!
Line 18
All query parts must share the same connection.
Line 19
The outer most QueryBuilder is responsible for the union, it must be
used to create named parameters and build expressions within the sub queries.
Line 22-23
We therefore pass the central QueryBuilder responsible for the
UNION
to all subqueries. Same with the ExpressionBuilder.
Line 25-30
We start building the union() on the first sub query, then add the
second sub query using addUnion()
Line 41
Only use the ExpressionBuilder of the sql:UNION within the subqueries.
Line 50
Named parameters must also be called on the outer most union query builder.
It is allowed to call
->setMaxResults() without calling
->setFirstResult().
It is possible to call
->setFirstResult() without calling
setMaxResults(): This is equivalent to "Fetch everything, but leave
out the first n records". Internally,
LIMIT will be added by
Doctrine DBAL and set to a very high value.
->setMaxResults(null) can be used to retrieve all results.
If an unlimited result set is needed, and no
reset of previous instructions is required, this method call should best
be omitted for best compatibility.
Changed in version 13.0
Starting with TYPO3 13 null instead of argument 0 (integer)
must be used in
->setMaxResults() to return
the complete result set without any
LIMIT.
add()
Changed in version 13.0
With the upgrade to Doctrine DBAL version 4 this method has been removed.
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:
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:
This is debugging code. Take proper actions to ensure that these calls do
not end up in production!
The method is usually called directly before
->executeQuery() or
->executeStatement() to output the final statement.
Doctrine DBAL always creates prepared statements: Each value added via
createNamedParameter
creates a placeholder that is later replaced when the real query is
triggered via
->executeQuery() or
->executeStatement().
->getParameters() does not show the statement or the placeholders,
instead the values are displayed, usually an array using keys like
:dcValue1. There is no simple solution to show the fully replaced query
within the framework, but you can use getSql to see the string with placeholders, which
is used as a prepared statement.
executeQuery() and executeStatement()
Changed in version 13.0
The
->execute() method has been removed. Use
->executeQuery() returning a
\Doctrine\DBAL\Result instead
of a
\Doctrine\DBAL\Statement (like the
->execute()
method returned) and
->executeStatement() returning the number of affected rows.
executeQuery()
This method compiles and fires the final query statement. This is usually the
last call on a query builder object. It can be called for
SELECT and
COUNT queries.
On success, it returns a result object of type
\Doctrine\DBAL\Result
representing the result set. The
Result object can then be used by
fetchAssociative(),
fetchAllAssociative() and
fetchOne().
executeQuery() returns a
\Doctrine\DBAL\Result and not a
\Doctrine\DBAL\Statement anymore.
Note
It is not possible to rebind placeholder values on the result and execute
another query, as was sometimes done with the
Statement returned by
execute().
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:
This object is stateless and can be called and worked on as often as needed.
However, it is bound to the specific connection for which a statement is
created and therefore only available through the query builder, which is
specific to a connection.
Never reuse the ExpressionBuilder,
especially not between multiple query builder objects, but always get an
instance of the expression builder by calling
->expr().
createNamedParameter()
Changed in version 13.0
Doctrine DBAL v4 dropped the support for using the
\PDO::PARAM_*
constants in favor of the enum types. Be aware of this and use
\TYPO3\CMS\Core\Database\Connection::PARAM_*, which can already be
used in TYPO3 v12 and v11.
This method creates a placeholder for a field value of a prepared statement.
Always use this when dealing with user input in expressions to protect the
statement from SQL injections:
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:
// NEVER EVER DO THIS!
$_POST['searchword'] = "'foo' UNION SELECT username FROM be_users";
$searchWord = $request->getParsedBody()['searchword']);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
this fails with syntax error to prevent copy and paste
$queryBuilder
->select('uid')
->from('tt_content')
->where(
// MASSIVE SECURITY ISSUE DEMONSTRATED HERE// USE ->createNamedParameter() ON $searchWord!
$queryBuilder->expr()->eq('bodytext', $searchWord)
);
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!
Note
->set() automatically converts the second mandatory parameter into
a named parameter of a prepared statement. If the second parameter is
wrapped in a
->createNamedParameter() call, this will result in an
error during execution. This behaviour can be disabled by passing
false as third parameter to
->set().
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:
->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 ':
// 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')
)
);
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:
Even when using
->escapeLikeWildcards() the value must be
encapsulated again in a
->createNamedParameter() call. Only calling
->escapeLikeWildcards() does not make the value SQL injection
safe!
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.
Note
The
Connection object is designed to work on a single table only. If
queries are performed on multiple tables, the object must not be reused.
Instead, a single
Connection instance per target table should be
retrieved via ConnectionPool. However, it
is allowed to use one
Connection object for multiple queries on the
same table.
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:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\Connection;
finalclassMyTableRepository{
publicfunction__construct(
private readonly Connection $connection,
){}
publicfunctionfindSomething(){
// 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);
The name of the table the row should be inserted. Required.
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.
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:
The name of the table the row should be inserted. Required.
An array of the values to be inserted. Required.
An array containing the column names of the data which should be inserted.
Optional.
Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted
to strings. Optional.
The number of inserted rows are returned. If something goes wrong, a
\Doctrine\DBAL\Exception is thrown.
Note
MySQL is quite forgiving when it comes to insufficient field quoting:
Inserting a string into an
int field does not cause an error and
MySQL adjusts internally. However, other DBMSes are not that relaxed and may raise errors. It is good practice to
specify field types for each field, especially if they are not strings.
Doing this immediately will reduce the number of bugs that occur when people
run your extension an anything else than MySQL.
update()
Create an
UPDATE statement and execute it. The example from FAL's
ResourceStorage sets a storage to offline:
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.
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.
Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted
to strings. Optional.
The method returns the number of updated rows. If something goes wrong, a
\Doctrine\DBAL\Exception is thrown.
Note
The third argument WHERE `foo` = 'bar' supports only equal =. For more
complex stuff the query builder must be used.
delete()
Execute a
DELETE query using equal conditions in
WHERE, example
from
BackendUtility, to mark rows as no longer locked by a user:
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.
Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted
to strings. Optional.
The method returns the number of deleted rows. If something goes wrong, a
\Doctrine\DBAL\Exception is thrown.
Note
TYPO3 uses a "soft delete" approach for
many tables. Instead of deleting a row directly in the database, a field -
often called
deleted - is set from 0 to 1. Executing a
DELETE
query circumvents this and really removes rows from a table. For most
tables, it is better to use the DataHandler API
to handle deletions instead of executing such low-level queries directly.
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:
The argument is the name of the table to be truncated. If something goes wrong,
a
\Doctrine\DBAL\Exception is thrown.
count()
This method executes a
COUNT query. Again, this becomes useful when very
simple
COUNT statements are to be executed. The example below returns the
number of active rows (not hidden or deleted or disabled by time) from the
table
tx_myextension_mytable whose
field
some_value field set to
$something:
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:
The columns of the table which to select as an array. Required.
The name of the table. Required.
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.
The columns to group the results by as an array. In SQL they are mapped
in a
GROUP BY keyword. Optional.
An associative array of column name/sort directions pairs. In SQL they are
mapped in an
ORDER BY keyword. Optional.
The maximum number of rows to return. In SQL it is mapped in a
LIMIT
keyword. Optional.
The first result row to select (when used the maximum number of rows). In
SQL it is mapped in an
OFFSET keyword. Optional.
In contrast to the other short-hand methods,
->select() returns a
Result object ready for
->fetchAssociative() to
get single rows or for
->fetchAllAssociative() to get all rows at once.
Remarks:
For non-trivial
SELECT queries it is often better to switch to the
according method of the query builder
object.
The restriction builder adds default
WHERE restrictions. If these restrictions do not match the query
requirements, it is necessary to switch to the
QueryBuilder->select()
method for fine-grained
WHERE manipulation.
lastInsertId()
Changed in version 13.0
The method no longer accepts the table name as first argument and the
name of the auto-increment field as second argument.
This method returns the
uid of the last insert() statement. This is useful if the ID is to be used
directly afterwards:
The last inserted ID needs to be retrieved directly before inserting a
record to another table. That should be the usual workflow used in the wild
- but be aware of this.
createQueryBuilder()
The query builder should not be reused for
multiple different queries. However, sometimes it is convenient to first fetch a
connection object for a specific table and execute a simple query, and later
create a query builder for a more complex query from that connection object. The
usefulness of this method is limited, however, and at the time of writing no
good example could be found in the Core.
The method can also be useful in loops to save some precious code characters:
TYPO3 Core's Database API based on Doctrine DBAL supports the native
database field type json, which is already available for all supported DBMS
of TYPO3 v12.
JSON-like objects or arrays are automatically serialized during writing a
dataset to the database, when the native JSON type was used in the database
schema definition.
By using the native database field declaration json in ext_tables.sql
file within an extension, TYPO3 converts arrays or objects of type
\JsonSerializable into a serialized JSON value in the database when
persisting such values via
Connection->insert() or
Connection->update() if no explicit DB types are handed in as additional
method argument.
TYPO3 now utilizes the native type mapping of Doctrine to convert special types
such as JSON database field types automatically for writing.
When reading a record from the database via QueryBuilder, it is still necessary
though to transfer the serialized value to an array or object doing custom
serialization.
The ExpressionBuilder class is responsible for dynamically creating parts of
SQL queries.
It builds query conditions, ensuring that table and column names
are quoted inside the expressions / SQL fragments. The class is a facade to
the Doctrine ExpressionBuilder.
The ExpressionBuilder is used in the context of the QueryBuilder to ensure
that queries conform to the requirements of whichever database platform is in use.
Basic usage
An instance of the ExpressionBuilder is retrieved from the
QueryBuilder object:
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:
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
Junctions
->and() conjunction
->or() disjunction
Combine multiple single expressions using
AND or
OR. Nesting is
possible, both methods are variadic and accept any number of arguments, which
are then all combined. It usually makes little sense to pass zero or only
one argument.
Creates an equality comparison expression with the given arguments.
param $fieldName
The fieldname. Will be quoted according to database platform automatically.
param $value
The value. No automatic quoting/escaping is done.
Returns
string
neq(string $fieldName, ?mixed $value)
Creates a non equality comparison expression with the given arguments.
First argument is considered the left expression and the second is the right expression.
When converted to string, it will generate a <left expr> <> <right expr>. Example:
->like() and
->notLike() values must be additionally
quoted with a call to $queryBuilder->escapeLikeWildcards($value) to suppress the special
meaning of % characters in $value.
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.
Value, identifier or expression which should be aliased
param $asIdentifier
Alias identifier, default: ''
Return description
Returns aliased expression
Returns
string
Creates a statement to append a field alias to a value, identifier or sub-expression.
Note
Some
ExpressionBuilder methods (like
select() and
from()) provide an argument to directly add
the expression alias to reduce nesting. This new method can be used for
custom expressions and avoids recurring conditional quoting and alias appending.
Returns the concatenation expression compatible with the database connection platform
Returns
string
Can be used to concatenate values, row field values or expression results into
a single string value.
The resulting value is built using a platform-specific and preferred
concatenation method, for example
field1 || field2 || field3 || ...
for SQLite and
CONCAT(field1, field2, field3, ...) for other database vendors.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
Quoted value or expression result which should be cast to integer type
param $asIdentifier
Optionally add a field identifier alias (AS), default: ''
Return description
Returns the integer cast expression compatible with the connection database platform
Returns
string
Can be used to create an expression which converts a value, row field value or
the result of an expression to a signed integer type.
Uses the platform-specific preferred way for casting to dynamic length
character type, which means
CAST("value" AS INTEGER) for most database vendors
except PostgreSQL. For PostgreSQL
"value"::INTEGER cast notation
is used.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'my_table';
publicfunction__construct(private readonly ConnectionPool $connectionPool){}
publicfunctiondemonstrateCastInt(): void{
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
$queryBuilder
->select('uid')
->from('pages');
// simple value (quoted) to be used as sub-expression
$expression1 = $queryBuilder->expr()->castInt(
$queryBuilder->quote('123'),
);
// simple value (quoted) to return as select field
$queryBuilder->addSelectLiteral(
$queryBuilder->expr()->castInt(
$queryBuilder->quote('123'),
'virtual_field',
),
);
// cast the contents of a specific field to integer
$expression3 = $queryBuilder->expr()->castInt(
$queryBuilder->quoteIdentifier('uid'),
);
// expression to be used as sub-expression
$expression4 = $queryBuilder->expr()->castInt(
$queryBuilder->expr()->castVarchar('(1 * 10)'),
);
// expression to return as select field
$queryBuilder->addSelectLiteral(
$queryBuilder->expr()->castInt(
$queryBuilder->expr()->castVarchar('(1 * 10)'),
'virtual_field',
),
);
}
}
Copied!
ExpressionBuilder::castText()
New in version 13.3
Can be used to create an expression which converts a value, row field value or
the result of an expression to type TEXT or a large `VARCHAR, depending on which
database system is in use.
Casting is done to large
VARCHAR/CHAR types using the
CAST/CONVERT
or similar methods based on the database engine.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
castVarchar(string $value, int $length = 255, string $asIdentifier = '')
param $value
Unquoted value or expression, which should be cast
param $length
Dynamic varchar field length, default: 255
param $asIdentifier
Used to add a field identifier alias (AS) if non-empty string (optional), default: ''
Return description
Returns the cast expression compatible for the database platform
Returns
string
Can be used to create an expression which converts a value, row field value or
the result of an expression to a varchar type with dynamic length.
Uses the platform-specific preferred way for casting to dynamic length
character type, which means
CAST("value" AS VARCHAR(<LENGTH>))
or
CAST("value" AS CHAR(<LENGTH>)), except for PostgreSQL
where
"value"::INTEGER cast notation is used.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
This method is used for "if-then-else" expressions. These are
translated into
IF or
CASE statements depending on the
database engine in use.
Warning
No automatic quoting or escaping is done for the condition and true/false
parts. Extension authors need to ensure proper quoting for each part or use
API calls to do the quoting, for example the
\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression or
ExpressionBuilder calls.
Integer value or expression providing the length as integer
param $value
Value, identifier or expression defining the value to extract from the left
param $asIdentifier
Provide AS identifier if not empty, default: ''
Return description
Return the expression to extract defined substring from the right side.
Returns
string
Extract
$length characters of
$value from the left.
Creates a
LEFT("value", number_of_chars) expression for all supported
database vendors except SQLite, which uses
substring("value", start[, number_of_chars])
to provide a compatible expression.
Tip
For other sub string operations,
\Doctrine\DBAL\Platforms\AbstractPlatform::getSubstringExpression()
can be used. Synopsis:
getSubstringExpression(string $string, string $start, ?string $length = null): string.
Left-pad the value or sub-expression result with
$paddingValue, to a total
length of
$length.
SQLite does not support
LPAD("value", length, "paddingValue"), therefore a
more complex compatible replacement expression construct is created.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'tt_content';
publicfunction__construct(private readonly ConnectionPool $connectionPool){}
publicfunctiondemonstrateLeftPad(): void{
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
// Left-pad "123" with "0" to an amount of 10 times, resulting in "0000000123"
$expression1 = $queryBuilder->expr()->leftPad(
$queryBuilder->quote('123'),
10,
'0',
);
// Left-pad contents of the "uid" field with "0" to an amount of 10 times, a uid=1 would return "0000000001"
$expression2 = $queryBuilder->expr()->leftPad(
$queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
10,
'0',
);
// Sub-expression to left-pad the concated string result ("1" + "2" + "3") up to 10 times with 0, resulting in "0000000123".
$expression3 = $queryBuilder->expr()->leftPad(
$queryBuilder->expr()->concat(
$queryBuilder->quote('1'),
$queryBuilder->quote('2'),
$queryBuilder->quote('3'),
),
10,
'0',
);
// Left-pad the result of sub-expression casting "1123" to a string,// resulting in "0000001123".
$expression4 = $queryBuilder->expr()->leftPad(
$queryBuilder->expr()->castVarchar('( 1123 )'),
10,
'0',
);
// Left-pad the result of sub-expression casting "1123" to a string,// resulting in "0000001123" being assigned to "virtual_field"
$expression5 = $queryBuilder->expr()->leftPad(
$queryBuilder->expr()->castVarchar('( 1123 )'),
10,
'0',
'virtual_field',
);
}
}
Statement or value defining how often the $value should be repeated. Proper quoting must be ensured.
param $value
Value which should be repeated. Proper quoting must be ensured
param $asIdentifier
Provide AS identifier if not empty, default: ''
Return description
Returns the platform compatible statement to create the x-times repeated value
Returns
string
Create a statement to generate a value which repears the
$value for
$numberOfRepeats times. This method can be used to provide the
repeat number as a sub-expression or calculation.
REPEAT("value", numberOfRepeats) is used to build this expression for all database
vendors except SQLite which uses
REPLACE(PRINTF('%.' || <valueOrStatement> || 'c', '/'),'/', <repeatValue>)
, based on
REPLACE() and the built-in
printf().
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'pages';
publicfunction__construct(private readonly ConnectionPool $connectionPool){}
publicfunctiondemonstrateRepeat(): void{
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
// Repeats "." 10 times, resulting in ".........."
$expression1 = $queryBuilder->expr()->repeat(
10,
$queryBuilder->quote('.'),
);
// Repeats "0" 20 times and allows to access the field as "aliased_field" in query / result
$expression2 = $queryBuilder->expr()->repeat(
20,
$queryBuilder->quote('0'),
$queryBuilder->quoteIdentifier('aliased_field'),
);
// Repeat contents of field "table_field" 20 times and makes it available as "aliased_field"
$expression3 = $queryBuilder->expr()->repeat(
20,
$queryBuilder->quoteIdentifier('table_field'),
$queryBuilder->quoteIdentifier('aliased_field'),
);
// Repeate database field "table_field" the number of times that is cast to integer from the field "repeat_count_field" and make it available as "aliased_field"
$expression4 = $queryBuilder->expr()->repeat(
$queryBuilder->expr()->castInt(
$queryBuilder->quoteIdentifier('repeat_count_field'),
),
$queryBuilder->quoteIdentifier('table_field'),
$queryBuilder->quoteIdentifier('aliased_field'),
);
// Repeats the character "." as many times as the result of the expression "7+3" (10 times)
$expression5 = $queryBuilder->expr()->repeat(
'(7 + 3)',
$queryBuilder->quote('.'),
);
// Repeat 10 times the result of a concatenation expression (".") and make it available as "virtual_field_name"
$expression6 = $queryBuilder->expr()->repeat(
'(7 + 3)',
$queryBuilder->expr()->concat(
$queryBuilder->quote(''),
$queryBuilder->quote('.'),
$queryBuilder->quote(''),
),
'virtual_field_name',
);
}
}
Integer value or expression providing the length as integer
param $value
Value, identifier or expression defining the value to extract from the left
param $asIdentifier
Provide AS identifier if not empty, default: ''
Return description
Return the expression to extract defined substring from the right side
Returns
string
Extract
$length characters of
$value from the right.
Creates a
RIGHT("value", length) expression for all supported
database vendors except SQLite, which uses
substring("value", start_of_string[, length]).
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
Right-pad the value or sub-expression result with
$paddingValue, to a
total length of
$length.
SQLite does not support
RPAD("value", length, "paddingValue"), therefore a
more complex compatible replacement expression construct is created.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'pages';
publicfunction__construct(private readonly ConnectionPool $connectionPool){}
publicfunctiondemonstrateRightPad(): void{
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
// Right-pad the string "123" up to ten times with "0", resulting in "1230000000"
$expression1 = $queryBuilder->expr()->rightPad(
$queryBuilder->quote('123'),
10,
'0',
);
// Right-pad the cnotents of field "uid" up to ten times with 0, for uid=1 results in "1000000000".
$expression2 = $queryBuilder->expr()->rightPad(
$queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
10,
'0',
);
// Right-pad the results of concatenating "1" + "2" + "3" ("123") up to 10 times with 0, resulting in "1230000000"
$expression3 = $queryBuilder->expr()->rightPad(
$queryBuilder->expr()->concat(
$queryBuilder->quote('1'),
$queryBuilder->quote('2'),
$queryBuilder->quote('3'),
),
10,
'0',
);
// Left-pad the result of sub-expression casting "1123" to a string,// resulting in "1123000000""
$expression4 = $queryBuilder->expr()->rightPad(
$queryBuilder->expr()->castVarchar('( 1123 )'),
10,
'0',
);
// Right-pad the string "123" up to 10 times with "0" and make the result ("1230000000") available as "virtual_field"
$expression5 = $queryBuilder->expr()->rightPad(
$queryBuilder->quote('123'),
10,
'0',
'virtual_field',
);
}
}
Statement or value defining how often a space should be repeated. Proper quoting must be ensured.
param $asIdentifier
Provide AS identifier if not empty, default: ''
Return description
Returns the platform compatible statement to create the x-times repeated space(s).
Returns
string
Create a statement containing
$numberOfSpaces space characters.
The
SPACE(numberOfSpaces) expression is used for MariaDB and MySQL and
ExpressionBuilder::repeat() expression as a fallback for PostgreSQL
and SQLite.
Warning
Make sure that you quote value identifiers and sub-expressions using
QueryBuilder methods like
quote(),
quoteIdentifier or
createNamedParameter.
Quotes are not added automatically.
It is very important to quote values correctly so that you don't introduce SQL
injection attack vectors into your application. See
this section of the query builder
for details.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Domain\Repository;
useTYPO3\CMS\Core\Database\ConnectionPool;
finalclassMyTableRepository{
privateconst TABLE_NAME = 'pages';
publicfunction__construct(private readonly ConnectionPool $connectionPool){}
publicfunctiondemonstrateSpace(): void{
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable(self::TABLE_NAME);
// Returns " " (10 space characters)
$expression1 = $queryBuilder->expr()->space(
'10',
);
// Returns " " (10 space characters) and makes available as "aliased_field"
$expression2 = $queryBuilder->expr()->space(
'20',
$queryBuilder->quoteIdentifier('aliased_field'),
);
// Return amount of space characters based on calculation (10 spaces)
$expression3 = $queryBuilder->expr()->space(
'(7+2+1)',
);
// Return amount of space characters based on a fixed value (210 spaces) and make available as "aliased_field"
$expression3 = $queryBuilder->expr()->space(
'(210)',
$queryBuilder->quoteIdentifier('aliased_field'),
);
// Return a space X times, where X is the contents of the field table_repeat_number_field
$expression5 = $queryBuilder->expr()->space(
$queryBuilder->expr()->castInt(
$queryBuilder->quoteIdentifier('table_repeat_number_field'),
),
);
$expression6 = $queryBuilder->expr()->space(
$queryBuilder->expr()->castInt(
$queryBuilder->quoteIdentifier('table_repeat_number_field'),
),
$queryBuilder->quoteIdentifier('aliased_field'),
);
}
}
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
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.
Note
Having this
DefaultRestrictionContainer used everywhere is the second
iteration of this code construct:
The first variant automatically added contextual restrictions. For instance,
a query triggered by a call in the backend did not add the hidden flag,
while a query triggered in the frontend did. We quickly figured out that
this leads to a huge mess: The distinction between frontend, backend and CLI
is not that sharp in TYPO3, so for example the frontend behaves much more
like a backend call when using the admin panel.
The currently active variant is much easier: It always adds sane defaults
everywhere, a developer only has to deal with details if they do not fit.
The Core Team hopes that this approach is a good balance between hidden
magic, security, transparency, and convenience.
The workspace restriction limits an SQL query to only select records which
are "online" and in live or current workspace.
Note
As an important note and limitation of any workspace-related restrictions,
fetching the exact records need to be handled after the SQL results are
fetched, by overlaying the records with
\TYPO3\CMS\Backend\Utility\BackendUtility::getRecordWSOL(),
\TYPO3\CMS\Core\Domain\Repository\PageRepository->versionOL() or
\TYPO3\CMS\Core\DataHandling\PlainDataResolver .
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.
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.
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.
SELECT"c1".*
FROM"tt_content""c1"LEFTJOIN"tt_content""c2"ON c1.parent_field = c2.uid
WHERE (("c2"."uid"ISNULL) 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:
The class name must be the array key and the value must always be an array,
which is reserved for options given to the restriction objects.
Attention
Restrictions added by third-party extensions will impact the whole system.
Therefore this API does not allow removing restrictions added by the system
and adding restrictions should be handled with care.
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:
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(();
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);
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.
Tip
It can be very helpful to debug the final statements created by the
RestrictionBuilder using
debug($queryBuilder->getSQL()) right
before the final call to
$queryBuilder->executeQuery(). Just take
care these calls do notend up in production code.
The return type of single field values is not type safe! If you select a
value from a field that is defined as
INT, the
Result result
may very well return that value as a PHP
string. This is also true
for other database column types like
FLOAT,
DOUBLE and others.
This is an issue with the database drivers used underneath. It may happen
that MySQL returns an integer value for an
INT field, while others
may return a string. In general, the application itself must take care of an
according type cast to achieve maximum DBMS compatibility.
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.
// 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
}
->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:
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:
// 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();
The Connection->count() implementation
does exactly that to return the number of rows directly.
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.
Warning
->rowCount() works well with
DELETE,
UPDATE and
INSERT queries. However, it does not return a valid number for
SELECT queries on some DBMSes.
Never use
->rowCount() on
SELECT queries. This may work with
MySQL, but will fail with other databases like SQLite.
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.
// 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
}
Prepare SELECT `uid` FROM `pages` WHERE `uid` = ?
Execute SELECT `uid` FROM `pages` WHERE `uid` = '24'
Execute SELECT `uid` FROM `pages` WHERE `uid` = '25'
Copied!
The log shows one statement preparation with two executions.
Doctrine DBAL supports custom driver middlewares since version 3. These
middlewares act as a decorator around the actual Driver component.
Subsequently, the Connection, Statement and Result components can be
decorated as well. These middlewares must implement the
\Doctrine\DBAL\Driver\Middleware interface.
A common use case would be a middleware to implement SQL logging capabilities.
<?phpdeclare(strict_types=1);
useMyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;
defined('TYPO3') ordie();
// Register a global middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-global-driver-middleware'] = [
'target' => CustomGlobalDriverMiddleware::class,
'after' => [
// NOTE: Custom driver middleware should be registered after essential// TYPO3 Core driver middlewares. Use the following identifiers// to ensure that.'typo3/core/custom-platform-driver-middleware',
'typo3/core/custom-pdo-driver-result-middleware',
],
];
// Disable a global driver middleware for a specific connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['my-ext/custom-global-driver-middleware']['disabled'] = true;
Copied!
Register a driver middleware for a specific connection
<?phpdeclare(strict_types=1);
useMyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;
defined('TYPO3') ordie();
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier'] = [
'target' => MyDriverMiddleware::class,
'after' => [
// NOTE: Custom driver middleware should be registered after essential// TYPO3 Core driver middlewares. Use the following identifiers// to ensure that.'typo3/core/custom-platform-driver-middleware',
'typo3/core/custom-pdo-driver-result-middleware',
],
];
Copied!
Registration for driver middlewares for TYPO3 v12 and v13
Extension authors providing dual Core support in one extension version can use
the
Typo3Version class to provide the configuration suitable for the Core
version and avoiding the deprecation notice:
TYPO3 makes the global and connection driver middlewares sortable
similar to the PSR-15 middleware stack. The available
structure for a middleware configuration is:
target
target
Data type
string
Required
yes
The fully-qualified class name of the driver middleware.
before
before
Data type
list of strings
Required
no
Default
[]
A list of middleware identifiers the current middleware should be registered
before.
after
after
Data type
list of strings
Required
no
Default
[]
A list of middleware identifiers the current middleware should be registered
after.
Note
All custom driver middlewares, global or connection-based, should be
placed after the 'typo3/core/custom-platform-driver-middleware' and
'typo3/core/custom-pdo-driver-result-middleware' driver middlewares to
ensure essential Core driver middlewares have been processed first.
disabled
disabled
Data type
boolean
Required
no
Default
false
It can be used to disable a global middleware for a specific connection.
Warning
Do not disable global driver middlewares provided by TYPO3 - they are
essential.
<?phpdeclare(strict_types=1);
useMyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;
defined('TYPO3') ordie();
// Register global driver middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['global-driver-middleware-identifier'] = [
'target' => CustomGlobalDriverMiddleware::class,
'disabled' => false,
'after' => [
// NOTE: Custom driver middleware should be registered after essential// TYPO3 Core driver middlewares. Use the following identifiers// to ensure that.'typo3/core/custom-platform-driver-middleware',
'typo3/core/custom-pdo-driver-result-middleware',
],
'before' => [
'some-driver-middleware-identifier',
],
];
// Disable a global driver middleware for a connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['global-driver-middleware-identifier'] = [
// To disable a global driver middleware, setting disabled to true for a// connection is enough. Repeating target, after and/or before configuration// is not required.'disabled' => true,
];
Copied!
Tip
If the lowlevel system extension is installed
and active, a Doctrine DBAL Driver Middlewares section is
provided in the System > Configuration module to view the raw
middleware configuration and the ordered middlewares for each connection:
Example of the configuration of driver middlewares
The interface
UsableForConnectionInterface
New in version 13.0
Note
Real use cases for this interface should be rare edge cases. Typically,
a driver middleware should only be configured on a connection where it is
needed - or does not harm, if used for all connection types as a global
driver middleware.
Doctrine DBAL driver middlewares can be registered
globally for all connections or for
specific connections. Due to the nature of
the decorator pattern, it may become hard to determine for a specific
configuration or drivers, if a middleware needs to be executed only for a
subset, for example, only specific drivers.
TYPO3 provides a custom
\TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface
driver middleware interface which requires the implementation of the method
canBeUsedForConnection():
Custom driver middleware can implement this interface to decide per connection and
connection configuration if it should be used or not. For example, registering a
global driver middleware which only takes affect on connections using a specific
driver like pdo_sqlite.
Usually this should be a rare case and mostly a driver middleware can be simply
configured as a connection middleware directly, which leaves this more or less
a special implementation detail for the TYPO3 core.
Return true if the driver middleware should be used for the concrete connection.
param $identifier
the identifier
param $connectionParams
the connectionParams
Returns
bool
This allows to decide, if a middleware should be used for a specific connection,
either based on the
$connectionName or the
$connectionParams,
for example the concrete
$connectionParams['driver'].
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DoctrineDBAL;
useDoctrine\DBAL\Driver\ConnectionasDriverConnection;
// Using the abstract class minimize the methods to implement and therefore// reduces a lot of boilerplate code. Override only methods that needed to be// customized.useDoctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
finalclassCustomDriverextendsAbstractDriverMiddleware{
publicfunctionconnect(#[\SensitiveParameter] array $params): DriverConnection{
$connection = parent::connect($params);
// Do something custom on connect, for example wrapping the driver// connection class or executing some queries on connect.return $connection;
}
}
Copied!
The custom driver middleware which implements the
\TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface :
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DoctrineDBAL;
useDoctrine\DBAL\DriverasDoctrineDriverInterface;
useDoctrine\DBAL\Driver\MiddlewareasDoctrineDriverMiddlewareInterface;
useMyVendor\MyExtension\DoctrineDBAL\CustomDriverasMyCustomDriver;
useTYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface;
finalclassCustomMiddlewareimplementsDoctrineDriverMiddlewareInterface, UsableForConnectionInterface{
publicfunctionwrap(DoctrineDriverInterface $driver): DoctrineDriverInterface{
// Wrap the original or already wrapped driver with our custom driver// decoration class to provide additional features.returnnew MyCustomDriver($driver);
}
publicfunctioncanBeUsedForConnection(
string $identifier,
array $connectionParams,
): bool{
// Only use this driver middleware, if the configured connection driver// is 'pdo_sqlite' (sqlite using php-ext PDO).return ($connectionParams['driver'] ?? '') === 'pdo_sqlite';
}
}
<?phpdeclare(strict_types=1);
useMyVendor\MyExtension\DoctrineDBAL\CustomMiddleware;
defined('TYPO3') ordie();
// Register middleware globally, to include it for all connections which// uses the 'pdo_sqlite' driver.
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-pdosqlite-driver-middleware'] = [
'target' => CustomMiddleware::class,
'after' => [
// NOTE: Custom driver middleware should be registered after essential// TYPO3 Core driver middlewares. Use the following identifiers// to ensure that.'typo3/core/custom-platform-driver-middleware',
'typo3/core/custom-pdo-driver-result-middleware',
],
];
Copied!
Various tips and tricks
Use Find usages of PhpStorm for examples! The source code of the
Core is a great way to learn how specific methods of the API are used. In
PhpStorm it is extremely helpful to right-click on a single method and list
all method usages with Find usages. This is especially handy
to quickly see usage examples for complex methods like
join()
from the query builder.
INSERT,
UPDATE and
DELETE statements are often better
to read and write using the Connection object
instead of the query builder.
SELECT DISTINCT aField is not supported but can be substituted with a
->groupBy('aField').
Doctrine DBAL throws exceptions if something goes wrong when calling API methods.
The exception type is
\Doctrine\DBAL\Exception. Typical extensions should
usually not catch such exceptions but let it bubble up to be handled by the
global TYPO3 core error and exception handling: They most often indicate a
broken connection, database schema or programming error and extensions should
usually not try to hide away or escalate them on their own.
count() <database-query-builder-count> query types using the
query builder normally call
->fetchOne() to receive the count value.
The count() method of the
Connection object does this automatically and
returns the result of the count value directly.
Troubleshooting
About database error "Row size too large"
MySQL and MariaDB can generate a "Row size too large" error when modifying
tables with numerous columns. TYPO3 version 13 has implemented measures
to mitigate this issue in most scenarios. We refer to the changelog:
Important: #104153 - About database error
"Row size too large".
Database compare during update and installation
Whenever you install or update an extension, or change the
TCA definition or the
ext_tables.sql in an extension, you have to take
into account the fact that the database schema might have changed.
Here system maintainers can compare the database schema and apply any changes.
Users with
System Maintainer privileges can use the
Analyze Database Structure section in the
Admin Tools > Maintenance module to compare the defined schema
with the current one. The module display options to incorporate changes by adding,
removing, or updating columns.
You can also use the console command typo3 extension:setup to add tables
and columns defined by installed or updated extensions:
vendor/bin/typo3 extension:setup
Copied!
typo3/sysext/core/bin/typo3 extension:setup
Copied!
Adding columns and tables is safe
Adding additional columns or tables is not problematic. You can safely add any
column shown as missing.
Deleting columns or tables: be careful
Columns suggested for deletion might still be needed by
upgrade wizards.
Before deleting tables or columns with the database analyzer:
Run all upgrade wizards
Make a database backup
Some third-party extensions may rely on database columns or tables they do not
explicitly define. Removing them could cause these extensions to break.
Changing a column type: it depends
Some column changes extend capabilities and are safe. For example:
Changing from
TEXT to
LONGTEXT allows more data to be stored
and does not affect existing content.
Other changes can cause problems if existing data violates the new definition.
For instance:
Changing from
NULL to
NOT NULL will fail if any row,
including soft-deleted ones (deleted = 1), still contains NULL.
Some extensions provide upgrade wizards to clean or convert data. Note that
many wizards ignore soft-deleted records. Deleting unnecessary soft-deleted
records may help.
If two extensions define the same column in different ways, the definition
from the extension that is
loaded last
will take precedence.
This means that an extension that changes or adds columns to a table must
declare a dependency on the original extension to ensure proper loading order.
Extbase persistence – models and the database
Extbase provides its own way to persist and retrieve data using models and
repositories, which are built on top of TYPO3's database abstraction layer.
Repositories in Extbase usually define custom find*() methods and rely on
\TYPO3\CMS\Extbase\Persistence\Generic\Query to perform queries on models.
While Extbase persistence is the standard way to work with data in Extbase,
you can also use the DBAL QueryBuilder directly within an Extbase context when:
You need better performance on large datasets.
You are performing complex queries (aggregates like
SUM,
AVG,
...).
Note
Extbase queries and DBAL queries are not interchangeable. Extbase uses its
own persistence layer with different concepts and behaviors. Use each
approach where it fits best — and avoid mixing them in the same method or query logic.
Reference index
The reference index in TYPO3 is the table sys_refindex.
(Related link: Soft references).
The table contains all relations/cross correlations between datasets.
For example a content element has an image and a link.
These two references can be found in this table stored against this
unique data record (tt_content uid).
When you want to perform a TYPO3 update it is recommended to update these relations.
See Update Reference Index.
To perform an update you can use the TYPO3 Console command shown in that section.
TYPO3 installations with a small number of records can use the module
System > DB check and use the Manage Reference Index
function.
On TYPO3 installations with a large number of records and many relations between
those the maximum run time of PHP will be reached and the scripts therefore
fail. It is recommended to run the commands from the command line. This
module outputs the commands with absolute paths to update or check the
reference from the command line.
The DataHandler is the class that handles alldata
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.
See also
The features of TCA are described in the TCA Reference.
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 .
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.
Caution
The DataHandler needs a properly configured TCA. If
your field is not configured in the TCA the DataHandler will not be able to
interact with it. This also is the case if you configured
"type"="none" (which is in fact a valid type) or if an invalid
type is specified. In that case, the DataHandler is not
able to determine the correct value of the field.
Basic usage
EXT:my_extension/Classes/DataHandling/MyClass.php
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionbasicUsage(): 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:
Any error that might have occurred during your DataHandler operations can be
accessed via its public property
$this->dataHandler->errorLog.
See Error handling.
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.
Note
Only one command can be executed at a time for each
record! The first command in the array will be taken.
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.
This action will set the "deleted" flag back to 0.
localize
localize
Data type
integer
The value is the
languageId (defined in the
site configuration) to localize the
record into. Basically a localization of a record is making a copy of the
record (possibly excluding certain fields defined with
l10n_mode) but
changing relevant fields to point to the right language ID.
Requirements for a successful localization is this:
A
languageId must be configured in the site configuration.
The record to be localized by currently be set to default language
and not have any value set for the TCA
transOrigPointerField either.
There cannot exist another localization to the given language for the
record (looking in the original record PID).
Apart from this, ordinary permissions apply as if the user wants to
make a copy of the record on the same page.
The
localize DataHandler command should be used when translating
records in "connected mode"
(strict translation of records from the default language). This command is
used when selecting the "Translate" strategy in the content elements
translation wizard.
copyToLanguage
copyToLanguage
Data type
integer
It behaves like localize command (both record and
child records are copied to given language), but does not set
transOrigPointerField
fields (for example,
l10n_parent).
The
copyToLanguage command should be used when localizing records in
the "free mode". This
command is used when localizing content elements using translation wizard's
"Copy" strategy.
inlineLocalizeSynchronize
inlineLocalizeSynchronize
Data type
array
Performs localization or synchronization of child records.
The command structure is like:
$cmd['tt_content'][13]['inlineLocalizeSynchronize'] = [ // 13 is a parent record uid'field' => 'tx_myfieldname', // field we want to synchronize'language' => 2, // uid of the target language// either the key 'action' or 'ids' must be set'action' => 'localize', // or 'synchronize''ids' => [1, 2, 3], // array of child IDs to be localized
];
Copied!
version
version
Data type
array
Versioning action.
Note
This section is currently outdated.
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.
Caution
The
$copyMappingArray_merged property should not be mixed up with
the
$copyMappingArray property which contains only information
about the last copy operation and is cleared between each operation.
The structure of the
$copyMappingArray_merged property looks like this:
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];
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').
Caution
If you supply your own string NEW must not be followed by an underscore.
The occurance of an underscore implies a reference to a record in a table.
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.
Note
For FlexForms the data array of the FlexForm field is
deeper than three levels. The number of possible levels for FlexForms
is infinite and defined by the data structure of the FlexForm. But
FlexForm fields always end with a "regular value" of course.
Caution
Changed in version 13.0.1/12.4.11/11.5.35
Modifying the
sys_file table using DataHandler is blocked since TYPO3
version 11.5.35, 12.4.11, and 13.0.1. The table
should not be extended and additional fields should be added to
sys_file_metadata. See security advisory TYPO3-CORE-SA-2024-006
for more information.
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:
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!
Note
To get real uid of the record you have just created use DataHandler's
substNEWwithIDs property like:
$uid = $this->dataHandler->substNEWwithIDs['NEW9823be87'];
This updates the page with uid=9834 to a new title, "New title for
this page", and no_cache checked:
EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages'][9834] = [
'title' => 'New title for this page',
'no_cache' => '1'
];
Copied!
Clear cache
DataHandler also has an API for clearing the cache tables of TYPO3:
EXT:my_extension/Classes/DataHandling/MyClass.php
$this->dataHandler->clear_cacheCmd($cacheCmd);
Copied!
Values for the
$cacheCmd argument:
[integer]
[integer]
Clear the cache for the page ID given.
"all"
"all"
Clears all cache tables (cache_pages, cache_pagesection,
cache_hash).
Only available for admin-users unless explicitly allowed by User
TSconfig "options.clearCache.all".
"pages"
"pages"
Clears all pages from cache_pages.
Only available for admin-users unless explicitly allowed by User
TSconfig "options.clearCache.pages".
Clear cache using cache tags
Every processing of data or commands is finalized with flushing a few caches in
the
pages group. Cache tags are used to specifically flush the
relevant cache entries instead of the cache as whole.
By default the following cache tags are flushed:
The table name of the updated record, for example,
pages when
updating a page or
tx_myextension_mytable when updating a record of
this table.
A combination of table name and record UID, for example,
pages_10
when updating the page with UID 10 or
tx_myextension_mytable_20 when
updating the record with UID 20 of this table.
A page UID prefixed with
pageID_ (
pageId_<page-uid>), for
example,
pageId_10 when updating a page with UID 10 (additionally all
related pages, see
clearcache-pagegrandparent
and
clearcache-pagesiblingchildren)
and
pageId_10 when updating a record if a record of any table placed
on the page with UID 10 (
<table>.pid = 10) is updated.
Notice that you can also use the \TYPO3\CMS\Core\Cache\CacheDataCollector::addCacheTags
method to register additional tags for the cache entry of the current page while
it is rendered. This way you can implement an elaborate caching behavior which
ensures that every record update in the TYPO3 backend (which is processed by the
DataHandler) automatically flushes the cache of all pages where that
record is displayed.
Following the rules mentioned above you could register cache tags
from within your Extbase plugin (for example, controller or a
custom ViewHelper):
The frontend.cache.collector
request attribut has been introduced as a successor of the now deprecated
TypoScriptFrontendController->addCacheTags() method. Switch to
another version of this page for an example in an older TYPO3 version. For
compatibility with TYPO3 v12 and v13 use
TypoScriptFrontendController->addCacheTags().
Hook for cache post-processing
You can configure cache post-processing with a user defined PHP
function. Configuration of the hook can be done from
ext_localconf.php. An example might look like:
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.
Attention
Mind that these scripts have to be run in the
backend scope! There must be a global
$GLOBALS['BE_USER'] object.
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:
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.
Warning
A new
DataHandler object should be created using
GeneralUtility::makeInstance(DataHandler::class) before each use.
It is a stateful service and has to be considered polluted after use. Do not
call
DataHandler::start() or
DataHandler::process_datamap()
multiple time on the same instance.
The
DataHandler class must not be injected into the constructor via
dependency injection. This can cause unexpected
side effects.
Submitting data
This is the most basic example of how to submit data into the database.
EXT:my_extension/Classes/DataHandling/MyClass.php
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionsubmitData(): 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
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionexecuteCommands(): 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).
Note
Clearing a given cache is possible only for users that are
"admin" or have specific permissions to do so.
EXT:my_extension/Classes/MyClass.php
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionclearCache(): 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:
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
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Backend\Utility\BackendUtility;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionsubmitComplexData(): 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
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
useTYPO3\CMS\Core\Authentication\BackendUserAuthentication;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunctionuseAlternativeUser(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
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\DataHandling;
usePsr\Log\LoggerInterface;
useTYPO3\CMS\Core\DataHandling\DataHandler;
useTYPO3\CMS\Core\Utility\GeneralUtility;
finalclassMyClass{
publicfunction__construct(
private readonly LoggerInterface $logger,
){}
publicfunctionhandleError(): 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".
To display additional debug information in the backend, set
$GLOBALS['TYPO3_CONF_VARS']['BE']['debug']
in config/system/settings.php and log in with an administrator
account.
It shows for example the names of fields and in case of select, radio and
checkbox fields the values in addition, which are generated by the
FormEngine. These can be used to set access permissions or
configuration using Page TSconfig.
If EXT:lowlevel is installed, the name of the
database table or field is appended to the select options in the
System > DB Check > Full Search module.
Additionally, in debug mode, the page renderer does not compress or concatenate
JavaScript or CSS resources.
DebugUtility::debug()
The TYPO3 Core provides a simple
debug() (defined in
EXT:core/Classes/Core/GlobalDebugFunctions.php). It wraps around
\TYPO3\CMS\Core\Utility\DebugUtility::debug() and will output debug
information only if it matches a set of IP addresses (defined in
$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']).
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.
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:
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>
Note
If you are debugging in a Fluid partial or a Fluid section, make sure that all variables you want to analyse are passed (defined in the arguments attribute of the render tag).
This chapter explains "Dependency Injection (DI)" as used in TYPO3. Readers
interested in the general concepts and principles may want to look at, for
example, Dependency Injection in "PHP The Right Way" and
What is dependency injection? by Fabien
Potencier. Whenever a service has a service dependency to another
class, the technique of dependency injection should be used to satisfy that need. TYPO3
uses a Symfony component for dependency injection. The component is PSR-11 compliant, and is used throughout
core and extensions to standardize the process of obtaining service dependencies.
By default all API services shipped with the TYPO3 Core system extensions offer dependency
injection. The recommended usage is constructor injection. Available as well are method injection and interface injection.
To activate the Symfony component for dependency injection a few lines of
configuration are necessary.
Introduction
The title of this chapter is "dependency injection" (DI), but the scope is a bit
broader: In general, this chapter is about TYPO3 object lifecycle management and
how to obtain services, with one sub-part of it being dependency injection.
Obtaining object instances in TYPO3 has always been straightforward: Call
GeneralUtility::makeInstance(\MyVendor\MyExtension\Some\Class::class) and hand over
mandatory and optional
__construct() arguments as additional arguments.
There are two quirks to that:
First, a class instantiated through
GeneralUtility::makeInstance() can implement
SingletonInterface. This empty interface definition tells makeInstance() to instantiate
the object exactly once for this request, and if another makeInstance() call asks for it,
the same object instance is returned - otherwise makeInstance() always creates a new object
instance and returns it. Implementing SingletonInterface is nowadays
considered old-fashioned, its usage should be reduced over time.
Second,
GeneralUtility::makeInstance() allows "XCLASSing". This is a - rather dirty -
way to substitute a class with a custom implementation. XCLASS'ing in general is
brittle and seen as a last resort hack if no better solution is available. In connection
with Symfony containers, XCLASSing services should be avoided in general and service
overrides should be used instead. Replacing the XCLASS functionality in TYPO3 is still work in progress.
In contrast, XCLASSing is still useful for data objects, and there is no good other solution yet.
Using
makeInstance() worked very well for a long time. It however lacked a feature
that has been added to the PHP world after
makeInstance() had been invented: Dependency injection.
There are lots of articles about dependency injection on the net, so we won't go too deep
here but rather explain the main idea: The general issue appears when classes follow the
separation of concerns
principle.
One of the standard examples is logging. Let's say a class's responsibility is the
creation of users - it checks everything and finally writes a row to database. Now, since
this is an important operation, the class wants to log an information like "I just created a user".
And this is where dependency injection enters the game: Logging is a huge topic, there are
various error levels, information can be written to various destinations and so on. The
little class does not want to deal with all those details, it just wants to tell the framework: "Please
give me some logger I can use and that takes care of all details, I don't want to know about them". This
separation is the heart of single responsibility and separation of concerns.
Dependency injection does two things for us here: First, it allows separating concerns, and second,
it hands the task of finding an appropriate implementation of a dependency over to the framework,
so the framework decides - based on configuration - which specific instance is given to the
consumer. Note in our example, the logging instance itself may have dependencies again - the process
of object creation and preparation may be nested.
In more abstract software engineering terms: Dependency injection
is a pattern used to delegate the task of resolving class dependencies away from a consumer
towards the underlying framework.
Back to history: After
makeInstance() has been around for quite a while and lacked an
implementation of dependency injection, Extbase appeared in 2009. Extbase brought a first container
and dependency injection solution, it's main interface being the Extbase
ObjectManager.
The Extbase object manager has been widely used for a long time, but suffered from some
issues younger approaches don't face. One of the main drawbacks of Extbase object manager
is the fact that it's based on runtime reflection: Whenever an object is to be instantiated,
the object manager scans the class for needed injections and prepares dependencies to be
injected. This process is quite slow though mitigated by various caches. And these also
come with costs. In the end, these issues have been the main reason the object manager
was never established as a main core concept but only lived in Extbase scope.
The object lifecycle and dependency injection solution based on Symfony DI has been
added in TYPO3v10 and is a general core concept: Next to the native
dependency injection, it is also wired into
makeInstance() as a long living
backwards compatibility solution, and it fully substitutes the Extbase object manager. In
contrast to the Extbase solution, Symfony based object management does not have the
overhead of
expensive runtime calculations. Instead it is an instance wide build-time solution: When
TYPO3 bootstraps, all object creation details of all classes are read from
a single cache file just once, and afterwards no expensive calculation is required
for actual creation.
Symfony based DI was implemented in TYPO3 v10 and usage of the Extbase
ObjectManager was discouraged.
The Extbase ObjectManager has been removed with TYPO3 v12. Making use of
Symfony DI integration continues. There
are still various places in the core to be improved. Further streamlining is
done over time. For instance, the final fate of
makeInstance()
and the
SingletonInterface has not fully been decided on. Various
tasks remain to be solved in younger TYPO3 development to further improve the
object lifecycle management provided by the core.
Build-time caches
To get a basic understanding of the core's lifecycle management it is helpful to
get a rough insight on the main construct. As already mentioned, object lifecycle
management is conceptualized as steps to take place at build-time.
It is done very early and only once during the TYPO3
bootstrap process. All calculated
information is written to a special cache that can not be reconfigured and is available
early. On subsequent requests the cache file is loaded.
Extensions can not mess with the construct if they adhere to the core API.
Besides being created early, the state of the container is independent and
exactly the same in frontend, backend and CLI scope. The same container instance may
even be used for multiple requests. This is becoming more and more important nowadays with the
core being able to execute sub requests. The only exception to this is the
Install Tool: It uses a more basic container that "cannot fail". This difference
is not important for extension developers however since they can't hook into the Install Tool
at those places.
The Symfony container implementation is usually configured to actively scan the
extension classes for needed injections. All it takes are just a couple
of lines within the Services.yaml file. This should be done within all extensions that
contain PHP classes and it is the fundamental setup we will outline in the following sections.
For developers, it is important to understand that dealing with Symfony DI is
an early core bootstrap and cached thing. The system will fail upon misconfiguration, leading
to unreachable frontend and backend.
Attention
Errors in the DI configuration may block frontend and backend!
The DI cache does not heal by itself but needs to be cleared manually!
With the container cache entry being a low level early bootstrap thing that is expensive
to calculate when it has to be rebuild, there is a limited list of options to flush
this cache:
The container cache entry is not deleted when a backend user clicks "Flush all caches"
in the backend top toolbar if the instance is configured as production
application. For developer convenience, the container cache is flushed in development
context, though.
The container cache is flushed using "Admin tools" -> "Maintenance" -> "Flush Caches"
of the Install Tool.
The container cache is flushed using the CLI command
vendor/bin/typo3 cache:flush. Using
vendor/bin/typo3 cache:warmup afterwards will rebuild and cache the container.
The container cache is automatically flushed when using the Extension Manager to load
or unload extensions in (non-Composer) classic mode.
Another way to quickly drop this cache during development is to remove all
var/cache/code/di/* files, which reside in typo3temp/ in classic mode
instances or elsewhere in composer mode instances (see Environment).
The main takeaway is: When a developer fiddles with container configuration,
the cache needs to be manually cleared. And if some configuration issue slipped in,
which made the container or DI calculation fail, the system does not heal itself and
needs both a fix of the Dependency injection
configuration plus probably a cache removal. The standalone Install
Tool however should always work, even if the backend breaks down, so the "Flush caches"
button is always reachable. Note that if the container calculation fails, the
var/log/typo3_* files contain the exception with backtrace!
Important terms
We will continue to use a couple of technical terms in this chapter, so let's quickly
define them to align. Some of them are not precisely used in our world, for
instance some Java devs may stumble upon "our" understanding of a prototype.
Prototype
The broad understanding of a prototype within the TYPO3 community is
that it's simply an object that is created anew every time. Basically the direct
opposite of a singleton. In fact, the prototype pattern describes a base object that
is created once, so
__construct() is called to set it up, after that it is
cloned each time one wants to have a new instance of it. The community isn't well aware
of that, and the core provides no "correct" prototype API, so the word prototype is
often misused for an object that is always created anew when the framework is
asked to create one.
Singleton
A singleton is an object that is instantiated exactly once within one request, with
the same instance being re-used when a class instance is requested. Symfony understands
such class instances as being "shared". TYPO3 can also declare a class as "shared" using the
SingletonInterface , but this is considered
old-fashioned. Services are usually declared shared by default.
This implies such classes should be stateless and there is trouble ahead when they are not.
Service
We use the understanding "What is a service?" from Symfony: In Symfony, everything
that is instantiated through the service container is a service. These are many things - for instance
controllers are services, as well as - non static - utilities, repositories and classes like mailers
and similar. To emphasize: Not only classes named with a
*Service suffix are services but basically
anything as long as it is not a data object. A class should represent either-or: A class is either
a service that manipulates or does something with given data and does not hold it, or is a class that
holds data. Sadly, this distinction is not always the case within TYPO3 core (yet), and there are
many classes that blend service functionality and data characteristics.
Data object
Data objects are the opposite of services. They are not available
through service containers. Calling
$container->has() returns
false and they can not be
injected. They should be instantiated using
new(). Domain models and DTOs are a typical example
of data objects. Data objects are not "service container aware" and do not support DI. Although the
TYPO3 core does not strictly follow this rule in all cases until now, the ambition is to get this done
over time.
Using DI
The general idea is: Whenever your service class has a service dependency to another class,
dependency injection should be used.
In some TYPO3 APIs dependency injection cannot be used yet. This applies to classes that need
specific data in their constructors or classes that are serialized and deserialized as, for
example, scheduler tasks. The TYPO3 core tries to refactor these cases over time. Such classes
need to fall back to old-school
GeneralUtility::makeInstance()
There are two ways proclaimed and natively supported by TYPO3 to obtain service dependencies:
Constructor injection using
__construct() and method injection using
inject*() methods. Constructor injection is the way to go as long as a class
is not dealing with complex abstract inheritance chains. The symfony service container
can inject specific classes as well as instances of interfaces.
Constructor injection
Lets say we're writing a controller that renders a list of users. Those users are
found using a
UserRepository service, making the user repository service a
direct dependency of the controller service. A typical constructor dependency injection
to resolve the dependency by the framework looks like this:
The symfony container setup process will now see
UserRepository as a dependency
of
UserController when scanning its
__construct() method. Since autowiring
is enabled by default (more on that below), an instance of the
UserRepository is
created and provided to
__construct() when the controller is created. The instance
is set as a class property using constructor
property promotion
and the property is declared
readonly.
Method injection
A second way to get services injected is by using
inject*() methods:
This ends up with basically the same result as above: The controller instance retrieves
an object of type
UserRepository in class property
$userRepository. The service
container calls such
inject*() methods directly after class instantiation, so after
__construct() has been executed, and before anything else.
The injection via methods was introduced by Extbase. TYPO3 core implemented it in addition to
the default Symfony constructor injection. Why did we do that, you may ask? Both
strategies have subtle differences: First, when using
inject*() methods, the type
hinted class property needs to be nullable, otherwise PHP >= 7.4 throws a warning
since the instance is not set during
__construct(). But that's just an
implementation detail. More important is an abstraction scenario. Consider this case:
We have an abstract controller service with a dependency plus a controller service that extends
the abstract and has further dependencies.
Now assume the abstract class is provided by TYPO3 core and the consuming class
is provided by an extension. If the abstract class would use constructor injection,
the consuming class would need to know the dependencies of the abstract, add its
own dependencies to the constructor, and then call
parent::__construct($logger) to
satisfy the abstracts dependency. This would hardcode all dependencies
of the abstract into extending classes. If later the abstract is changed and
another dependency is added to the constructor, this would break consuming
classes.
Differently put: When core classes "pollute"
__construct() with dependencies,
the core can not add dependencies without being breaking. This is the reason why
for example the extbase
AbstractController uses
inject*() methods for its
dependencies: Extending classes can then use constructor injection, do not need
to call
parent::__construct(), and the core is free to change dependencies of
the abstract.
In general, when the core provides abstract classes that are expected to be
extended by extensions, the abstract class should use
inject*() methods instead of
constructor injection. Extensions of course can follow this idea in similar
scenarios.
This construct has some further implications: Abstract classes should
think about making their dependency properties
private, so extending classes
can not rely on them. Furthermore, classes that should not be extended by extensions
are free to use constructor injection and should be marked
final, making
sure they can't be extended to allow internal changes.
Notice the difference? The code requests the injection of an interface and not a class!
This is permissible with both constructor and method injection. It compels the
service container to determine which specific class is configured as the
implementation of the interface and inject an instance of that class. A class
can declare itself as the default implementation of such an interface. This is
the essence of dependency injection: a consuming class no longer relies on a
specific implementation but on an interface's signature.
The framework ensures that something fulfilling the interface is injected. The
consuming class remains unaware of the specific implementation, focusing solely
on the interface methods. An instance administrator can configure the framework
to inject a different implementation, either globally or for specific classes.
The consumer remains unconcerned, interacting only with the interface methods.
The example below has a couple of controller classes as service consumers. There
is a service interface with a default implementation. The default implementation
uses the symfony PHP attribute
AsAlias to register itself as default.
A Services.yaml file configures different service implementation for
some service consumers:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Service;
classMyOtherServiceImplementationimplementsMyServiceInterface{
publicfunctionfoo(){
// do something
}
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:_defaults:autowire:trueautoconfigure:truepublic:false# Within MySecondController and MyThirdController different implementations# than the default MyDefaultServiceImplementation of MyServiceInterface# shall be injected.# When working with constructor injectionMyVendor\MyExtension\Controller\MySecondController:arguments:$service:'@MyVendor\MyExtension\Service\MyOtherServiceImplementation'# When working with method injectionMyVendor\MyExtension\Controller\MyThirdController:calls:-method:'injectMyService'arguments:$service:'@MyVendor\MyExtension\Service\MyOtherServiceImplementation'
Copied!
Configuration
Services.yaml declaring service defaults
Extensions have to configure their classes to make use of
dependency injection. This can be done in Configuration/Services.yaml.
Alternatively, Configuration/Services.php can also be used, if
PHP syntax is required to apply conditional logic to definitions.
A basic Services.yaml file of an extension looks like the following.
Note
Whenever the service configuration or class dependencies change, the Core
cache must be flushed, see above for details.
autowire: true instructs the dependency injection component to
calculate the required dependencies from type declarations. The calculation
generates service initialization code.
An extension is not required to use autowiring. It can manually wire
dependencies. However, opting out of autowiring is less convenient and is not
further documented in this guide.
autoconfigure
This directive instructs the dependency injection component to automatically add
Symfony service tags based on implemented interfaces and base classes. For instance,
autoconfiguration ensures that classes implementing
SingletonInterface are publicly available from
the Symfony container and marked as shared (
shared: true).
TYPO3 dependency injection relies on this this for various default configurations.
It is recommended to set
autoconfigure: true.
public
public: false is a performance optimization and should therefore be set
in extensions. This settings controls which services are available through the
dependency injection container used internally by
GeneralUtility::makeInstance().
See "What to make public?" for more information.
Model exclusion
The path exclusion
exclude: '../Classes/Domain/Model/*' excludes
your models from the dependency injection container: You cannot inject them
nor inject dependencies into them. Models are not services but data objects
and therefore should not require dependency injection. Also, these objects are
usually created by the Extbase persistence layer, which does not support the
DI container.
Autoconfiguration using attributes and Services.yaml
Single service classes may need to change auto configuration
to be different than above declared defaults. This can be done using PHP attributes.
The most common use case is
public: true:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Service;
useSymfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* This service is instantiated using GeneralUtility::makeInstance()
* in some cases, which requires 'public' being set to 'true'.
*/#[Autoconfigure(public: true)]
readonly classMyServiceUsingAutoconfigurePublicTrue{
publicfunction__construct(
private SomeDependency $someDependency,
){}
}
Copied!
The above usage of the
Autoconfigure attribute declares this service as
public: true which overrides a
public: false default from a
Services.yaml file for this specific class.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Service;
useSymfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* This service is stateful and configures the service container to
* inject new instances to consuming services when they are instantiated.
*/#[Autoconfigure(shared: false)]classMyServiceUsingAutoconfigureSharedFalse{
private string $foo = 'foo';
publicfunction__construct(
private readonly SomeDependency $someDependency,
){}
publicfunctionsetFoo(): void{
$this->foo = 'bar';
}
}
Copied!
It is possible to set both using
#[Autoconfigure(public: true, shared: false)].
The
Autoconfigure attribute is beneficial when an extension includes a
service class that is either stateful or instantiated using
GeneralUtility::makeInstance(). This attribute embeds the configuration
directly within the class file, eliminating the need for additional entries in
Services.yaml - the configuration is "in place".
To reconfigure "foreign" services - those not provided by the extension itself
but by another extension (such as a service class from ext:core) - the
Services.yaml file can be utilized. A common scenario is when a core
service is not declared public because all core extensions retrieve instances
via constructor or method injection, rather than
GeneralUtility::makeInstance(). If an extension must use
GeneralUtility::makeInstance() for a specific reason, it can declare
the "foreign" service as "public" in Services.yaml:
EXT:my_extension/Configuration/Services.yaml
services:_defaults:autowire:trueautoconfigure:truepublic:falseMyVendor\MyExtension\:resource:'../Classes/*'# Declare a "foreign" service "public: true" since this extension needs# to instantiate the service using GeneralUtility::makeInstance() and# the service is configured "public: false" by the extension delivering# that service.TYPO3\CMS\Core\Some\Service:public:true
Copied!
Autowiring using attributes
Autowiring, particularly the
Autowire PHP attribute, is a powerful tool
for making dependency injection more convenient and transparent. TYPO3 core
includes default configurations that facilitate its use. Let’s explore some
examples.
Consider a service performing an expensive operation that caches the result
within the TYPO3 runtime cache to avoid repeating the operation within the same
request. The runtime cache, being a dependent service, should be injected. A
naive approach is to inject the core
CacheManager and retrieve the
runtime cache instance:
The "cache.runtime" service alias, configured by the TYPO3 core extension,
performs the
CacheManager->getCache() operation behind the scenes.
Utilizing such shortcuts simplifies the consumers.
The autowire attribute also enables the execution of expressions and injection
of the results, which is useful for "compile-time" state that remains constant
during requests. For example, to inject a feature toggle status:
Another example, including alias definition, is new in TYPO3 v13. It enables
injecting values from ext_conf_templates.txt files using the
ExtensionConfiguration API.
This example demonstrates the combination of a service class with an alias and
a consumer utilizing this alias in an
Autowire attribute.
The TYPO3 core provides a couple such service aliases, with the above ones being
the most important ones for extension developers. TYPO3 core does
not arbitrarily add aliases.
Installation-wide configuration
A global service configuration for a project can be set up to be utilized across
multiple project-specific extensions. This allows, for example, the aliasing of
an interface with a concrete implementation that can be used in several
extensions. Additionally, project-specific CLI commands can be registered without the need for a
project-specific extension.
However, this is only possible - due to security restrictions - if TYPO3 is
configured such that the project root is outside the document root, which is
typically the case in Composer-based installations.
In Composer-based installations, the global service configuration files
services.yaml and services.php are located within the
config/system/ directory of a TYPO3 project.
Consider the following scenario: You want to use the PSR-20 clock interface as
a type hint for dependency injection in the service classes of your project's
various extensions. This setup allows the concrete implementation to change
without altering your code. In this example, we use lcobucci/clock as the
concrete implementation.
The global files services.yaml and services.php are read before
files from extensions. The global files can provide defaults but can not override
service definitions from service configuration files loaded afterwards.
In short: "Manually" instantiated services using
GeneralUtility::makeInstance(MyService::class) must be made
public.
The basic difference between public and private is well explained in the
symfony documentation:
When defining a service, it can be made to be public or private. If a service is
public, it means that you can access it directly from the container at runtime.
For example, the doctrine service is a public service:
// only public services can be accessed in this way
$doctrine = $container->get('doctrine');
Copied!
But typically, services are accessed using dependency injection. And in this case,
those services do not need to be public.
So unless you specifically need to access a service directly from the container
via
$container->get(), the best-practice is to make your services private.
The implementation of
GeneralUtility::makeInstance() utilizes
$container->get().
As a result, services instantiated using
makeInstance()must be declared public if they have
dependencies that need to be injected.
Services without dependencies can be instantiated using
makeInstance() without
the service made public, as they are instantiated using
new without constructor
arguments.
Some services are automatically declared public by basic TYPO3 dependency injection
configuration since they are instantiated using
makeInstance() by the core
framework. The most common ones are:
Extbase controllers implementing
ControllerInterface ,
usually by inheriting
ActionController . They are
additionally declared
shared: false.
Backend controllers with
AsController class attribute.
They are additionally declared
shared: false:
Services that use dependency injection and are not declared public typically error
out with typical messages when instantiated using
makeInstance() They should
be declared public:
Unsatisfied constructor injection
(1/1) ArgumentCountError
Too few arguments to function MyVendor\MyExtension\Namespace\Class::__construct(),
0 passed in typo3/sysext/core/Classes/Utility/GeneralUtility.php on line 3461 and exactly 1 expected
Copied!
Unsatisfied method injection
(1/1) Error
Call to a member function methodName() on null
Copied!
How to override service arguments?
Some services in the TYPO3 core use service arguments, which can be overridden by third-party extensions.
For example, the
$rateLimiterFactory argument in the
\ControllerPasswordRecoveryController of the
typo3/cms-felogin
extension uses a service with the ID
feloginPasswordRecovery.rateLimiterFactory.
This service is defined in the
Services.yaml file of
ext:felogin and includes a service
argument named
$config, which specifies the configuration for the Symfony Rate Limiter used in
the class.
A third-party extension can override the
feloginPasswordRecovery.rateLimiterFactory service in
its own Services.yaml file, as shown in the example below:
It is important that the 3rd party extension is loaded after the extension whose service it overrides.
This can be achieved by requiring the original extension as a dependency in the packages/my-extension/composer.json file
of the 3rd party extension.
What do declare
shared: false?
Attention
In short: Declare a service
shared: false if it is stateful.
A service declared
shared: false is not a singleton. Instead, a new instance
is created each time the consuming service is instantiated. This approach makes the
consuming service stateful as well, but declaring
shared: false can help
mitigate side effects between different services. It is often preferable to create
stateful services using
GeneralUtility::makeInstance() when needed, rather
than within
__construct().
When to use
GeneralUtility::makeInstance()?
Attention
In short: Use
GeneralUtility::makeInstance() to obtain instances of stateful
services within otherwise stateless services.
Ideally, all services in a framework are stateless: They depend
on other stateless services and are always retrieved using dependency injection.
TYPO3 core development is gradually transitioning more services to be stateless.
However, many historically stateful services still exist. The critical point is that
injecting a stateful service into a stateless service makes the consumer stateful as
well. This can create a chain of coupled stateful services, leading to unexpected
results when these services are reused multiple times within a single request. While
declaring a service
shared: false can mitigate the issue, it doesn't solve the
underlying problem. This scenario is a primary use case for
GeneralUtility::makeInstance(). Instead of injecting a stateful service at
service build time and reusing it frequently, the service can use
makeInstance() at runtime when it needs a service instance.
For instance, the
DataHandler class should
create new instances for each use, as it becomes "tainted" after use and cannot reset
its state properly. Such "dirty-after-use" services should be instantiated anew with
makeInstance() when needed.
Some services are stateful but provide workarounds to be injectable. A good example
is the Extbase
UriBuilder . It is
stateful due to its use of the method chaining pattern but includes a
reset()
method to reset its state. When used correctly, this service can be injected and
reused. Additionally,
UriBuilder is declared
shared: false, so
consumers receive distinct instances, reducing the risk of bugs from improper use of
reset().
Various solutions exist to make existing services stateless. For instance, the extbase
UriBuilder could deprecate its
setX() chaining methods and introduce a
UriParameterBag data object, which would be passed to the service worker methods.
Implementing such changes in the TYPO3 core codebase is an ongoing process that requires
careful consideration.
Deciding whether to use
makeInstance() instead of dependency injection
requires examining the dependency's behavior. Consider these factors:
The service class is declared
readonly and only declares stateless
dependencies in
__construct().
The service has no class properties.
All
__construct() arguments are services and declared
readonly.
__construct() requires no manual non-service arguments.
The last point is particularly relevant: Some TYPO3 core services expect state to be
passed to
__construct(), making them stateful and unsuitable for injection,
as dependency injection cannot handle consumer state. These services must be
instantiated using
makeInstance() until their constructors are updated to be
compatible with dependency injection.
When to use
new?
Attention
In short: Use
new to instantiate data objects, not services.
Services should be always retrieved using dependency injection. If that is not
feasible because the dependent service is stateful or because the class is created
using a "polluted" constructor with manual arguments, it should be created using
makeInstance(). While services without dependencies could be instantiated
with
new, this approach has drawbacks: It introduces risks if the service
is later modified to include dependencies and bypasses the XCLASS mechanism and
potential service overrides by configuration.
Mix manual constructor arguments and service dependencies?
Attention
In short: No. For good reason.
A service can not mix manual constructor arguments with service dependencies
handled by dependency injection. Manual constructor arguments make services stateful.
When a service is instantiated with manual arguments, such as
$myService = GeneralUtility::makeInstance(MyService::class, $myState),
dependency injection is bypassed, and any other service dependencies in the
constructor are ignored. Mixing both blends the roles of services and data objects,
which is poor PHP architecture.
The extbase-based dependency injection solution using
ObjectManager
allowed such mixtures, but this has been replaced by the Symfony-based
dependency injection solution, which does not support this practice.
What about user functions?
It is possible to use dependency injection when calling custom user functions,
for example .userFunc within TypoScript or
in (legacy) hooks, usually via
\TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction().
callUserFunction() internally uses the dependency-injection-aware
helper
GeneralUtility::makeInstance(), which can recognize and inject
services that are marked public.
What about injection in a XCLASS'ed class?
When extending an existing class (for example, an Extbase controller) using
XCLASS and injecting additional dependencies using constructor
injection, define the existing class as an alias for the extended class in the
Configuration/Services.yaml file of the extending extension, by using the
shortcut notation for an alias as shown in the example below:
If the extended class is instantiated by
GeneralUtility::makeInstance() and
must be declared public, either use an additional
PHP attribute or the full alias
notation including the public argument:
This document does not currently elaborate on Symfony service providers,
although the TYPO3 core uses them in various places. Use cases for these
should be outlined.
The concept and usage of "lazy" services are not discussed.
Solutions to cyclic dependencies should be explored. Cyclic dependencies
occur when services depend on each other, forming a graph instead of a
tree, which Symfony's dependency injection cannot resolve. One solution
is to make one side lazy, although this is not the primary use of
"lazy." Another approach involves using a factory with an interface,
as demonstrated in ext:styleguide.
Deprecation handling: logging, marking and finding deprecations
TYPO3 logs calls to deprecated functions to help developers identify and update
outdated code. Deprecated methods will be removed in future TYPO3 versions, so
they should be avoided.
Deprecations are triggered by
trigger_error() and pass through TYPO3’s
logging and exception system. In development, they are shown as exceptions by
default; in production, they are typically ignored.
TYPO3 ships with a default configuration where deprecation logging is
disabled. If you upgrade to the latest TYPO3 version, you need to change
your development configuration to enable deprecation logging if you need
it.
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
The debug preset also enables some other debug settings.
Note
These steps only enable/disable the FileWriter,
which comes with the TYPO3 default configuration. If you manually configured
additional writers for the TYPO3.CMS.deprecations logger, you need to
manually remove them to completely disable deprecation logging.
Via configuration file directly
Instead of using the GUI you can also enable
or disable the deprecation log with the
disabled option:
Excerpt of config/system/settings.php | typo3conf/system/settings.php
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
*/publicfunctiondecreaseColPosCountByRecord(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
}
The TYPO3 Core includes an environment class that contains all
environment-specific information, mostly paths within the
filesystem. This implementation replaces previously used global variables and
constants like
PATH_site that have been removed with TYPO3 v10.
The fully qualified class name is
\TYPO3\CMS\Core\Core\Environment . The
class provides static methods to access the necessary information.
To simulate environments in testing scenarios, the
initialize()-method can
be called to adjust the information.
A comprehensive list of methods can be found in the
Class Reference.
getProjectPath()
The environment provides the path to the folder containing the composer.json.
For projects without Composer setup, this is equal to getPublicPath().
getPublicPath()
The environment provides the path to the public web folder with
index.php for the TYPO3 frontend. This was previously
PATH_site.
For projects without Composer setup, this is equal to getProjectPath().
getVarPath()
The environment provides the path to the var/ folder. This folder contains
data like logs, sessions, locks, and cache files.
For Composer-based installations, it returns var/, in Classic mode
installations typo3temp/var/.
In Composer-based installation this method provides the path
config/, in Classic mode installations
typo3conf/.
The directory returned by this method contains the folders system/
containing the configuration filessystem/settings.php and system/additional.php and the folder
sites/ containing the site configurations.
The environment provides the path to var/labels/ in
Composer-based installations, respective typo3conf/l10n/
folder in Classic mode installations. This folder contains downloaded translation files.
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.
useTYPO3\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.
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:
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.
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.
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 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 to the sys_log table. Logged errors are displayed
in the belog extension (Admin Tools > Log). This will only work with an
existing DB connection.
Depending on the Logging writer configuration
the exception output can be found for example in the following locations:
\TYPO3\CMS\Core\Log\Writer\FileWriter
In Composer-based installations the information can be found in directory
var/logs/. In Classic mode installations in typo3temp/var/logs/.
\TYPO3\CMS\Core\Log\Writer\SyslogWriter
Logs exception messages to the
sys_log table. Logged errors are displayed
in the backend module Admin Tools > Log.
The generic error message "Oops, an error occurred!" is displayed when an
exception or error happens within a TypoScript content object like
FLUIDTEMPLATE or a plugin. When the exception
affects only one content element or plugin it is displayed in place of that
elements. However, if it affects the content element representing the whole page
like PAGEVIEW only a plain page with this text on
it is displayed.
This message is displayed in production context
instead of a more detailed exception message. The detailed message can then be
found in the log.
Show detailed exception output
When the frontend debugging is activated, a detailed exception message is output
instead of the generic "Oops, an error occurred!" message.
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
Attention
Do not use debug / development setup in production. This setup generates error
messages in the Frontend and a number of log messages for low severity errors.
The messages in the Frontend will be visible to the user, give a potential attacker
more information about your system and the logging will fill your filesystem / DB,
which degrades performance and can potentially be used to bring down your system
by filling storage with log messages. See Use staging servers for developments and tests for more
information.
Very verbose configuration which logs and displays all errors and
exceptions.
In config/system/settings.php or config/system/additional.php:
Use this setting, to get more context and a stacktrace in the Frontend in case of an exception.
Attention
Do not set config.contentObjectExceptionHandler to 0 in production. It will
display a complete stack dump in the Frontend, when an exception occurs. Use
config.contentObjectExceptionHandler = 1, which is the default, in production.
Example for a production configuration which displays only errors and
exceptions, if the devIPmask setting
matches. Errors and exceptions are only logged, if their
log level is at least
\Psr\Log\LogLevel::WARNING.
In config/system/settings.php or config/system/additional.php:
Since the error and exception handling and also the logging need some
performance, here's an example how to disable error and exception
handling completely.
In config/system/settings.php or config/system/additional.php:
PHP warnings, among other things, are added to the TYPO3 log via
the Writer configuration of the Logging API.
In production or from a performance perspective, you may not want this.
The default value is
\Psr\Log\LogLevel::WARNING.
Depending on project requirements, the loglevel can be increased to
\Psr\Log\LogLevel::ERROR
(or higher).
In .htaccess:
.htaccess
php_flag display_errors offphp_flag log_errors off
Copied!
How to extend the error and exception handling
If you want to register your own error or exception handler:
Create a corresponding class in your extension
Override the Core defaults for productionExceptionHandler, debugExceptionHandler
or errorHandler in config/system/additional.php:
We use config/system/additional.php and notext_localconf.php
in the extension (as previously documented) because that will be executed
after the error / exception handlers are initialized in the bootstrap process.
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:
Events and hooks provide an easy way to extend the functionality of the TYPO3
Core and its extensions without blocking others to do the same.
Events are being emitted by the TYPO3 Core or an
extension via the event dispatcher. The event will be
received by all implemented event listeners for the event in question. Events
are strongly typed. Events only allow changes to variables that are intended to
be changed by the event.
Hooks are basically places in the source code where a
user function will be called for processing, if such has been configured.
Changed in version 12.0
Signals and slots and all related classes have been removed from the
Core. Use PSR-14 events instead.
TYPO3 extending mechanisms video
Lina Wolf: Extending Extensions @ TYPO3 Developer Days 2019
Events and hooks vs. XCLASS extensions
Events and hooks are the recommended way of extending TYPO3 compared to
extending PHP classes with a child class (see
XCLASS extensions). Using the XCLASS functionality only one
extension of a PHP class can exist at a time while hooks and events
may allow many different user-designed processor functions to be executed.
With TYPO3 v10 the event dispatcher was introduced. It is a strongly typed
way of extending TYPO3 and therefore recommended to use wherever available.
However, events have to be emitted and hooks have to be implemented in the TYPO3
Core or an extension before they can be used, while extending a PHP class via
the XCLASS method allows you to extend any class you like.
Proposing events
If you need to extend the functionality of a class which has no event or hook
yet, then you should suggest emitting an event. Normally that is rather easily
done by the author of the source you want to extend:
For a third-party extension create an issue in the according issue tracker
of that extension.
Event dispatcher (PSR-14 events)
The event dispatcher system was added to extend TYPO3's Core behaviour in
TYPO3 v10.0. In the past, this was done via Extbase's signal/slot and TYPO3's
custom hook system. The event dispatcher system is a fully-capable replacement
for new code in TYPO3, as well as a possibility to migrate away from previous
TYPO3 solutions.
Don't get hooked, listen to events! PSR-14 within TYPO3 v10.
-- Benni Mack @ TYPO3 Developer Days 2019
For a basic example on listening to an event, see the chapter
Listen to an event in the
extension development how-to section.
This quick start section shows how to create your own event class and dispatch it.
If you just want to listen on an existing event, see section
Implementing an event listener in your extension.
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:
PSR-14 is a lean solution that builds upon wide-spread solutions for hooking
into existing PHP code (Frameworks, CMS, and the like).
PSR-14 consists of the following four components:
The event dispatcher object
The
EventDispatcher object is used to trigger an event. TYPO3 has a
custom event dispatcher implementation. In PSR-14 all event dispatchers of all
frameworks are implementing
\Psr\EventDispatcher\EventDispatcherInterface , thus it is possible to
replace the event dispatcher with another. The
EventDispatcher's main
method
dispatch() is called in TYPO3 Core or extensions. It receives a
PHP object which will then be handed to all available listeners.
The listener provider
A
ListenerProvider object that contains all listeners which have been
registered for all events. TYPO3 has a custom listener provider that collects
all listeners during compile time. This component is only used internally inside of
TYPO3's Core Framework.
The events
An
Event object can be any PHP object and is called from TYPO3 Core or
an extension ("emitter") containing all information to be transported to the
listeners. By default, all registered listeners get triggered by an event,
however, if an event has the interface
\Psr\EventDispatcher\StoppableEventInterface implemented, a listener can
stop further execution of other event listeners. This is especially useful, if
the listeners are candidates to provide information to the emitter. This allows
to finish event dispatching, once this information has been acquired.
If an event allows modifications, appropriate methods should be available, although
due to PHP's nature of handling objects and the PSR-14 listener signature, it
cannot be guaranteed to be immutable.
Extensions and PHP packages can add listeners that are registered via YAML. They
are usually associated to
Event objects by the fully-qualified class name
of the event to be listened on. It is the task of the listener provider to
provide configuration mechanisms to represent this relationship.
If multiple event listeners for a specific event are registered, their order can
be configured or an existing event listener can also be overridden with a different one.
The System > Configuration > Event Listeners (PSR-14) backend module (requires
the system extension lowlevel) reveals an overview of all registered
event listeners, see Debugging event handling.
Advantages of the event dispatcher over hooks
The main benefits of the event dispatcher approach over hooks is an
implementation which helps extension authors to better understand the
possibilities by having a strongly typed system based on PHP. In addition, it
serves as a bridge to also incorporate other events provided by frameworks that
support PSR-14.
Impact on TYPO3 Core development in the future
TYPO3's event dispatcher serves as the basis to replace all
hooks in the future. However, for the time being, hooks
work the same way as before, unless migrated to an event dispatcher-like code,
whereas a PHP
E_USER_DEPRECATED error can be triggered.
Some hooks might not be replaced 1:1 to event dispatcher, but rather superseded
with a more robust or future-proof API.
Implementing an event listener in your extension
Hint
For a basic example on listening to an event, see the chapter
Listen to an event in the
extension development how-to section.
An extension can define multiple listeners. The attribute can be used on class
and method level. The PHP attribute is repeatable, which allows to register the
same class to listen for different events.
Once the emitter is triggering an event, this listener is called automatically.
Be sure to inspect the event's PHP class to fully understand the capabilities
provided by an event.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener supports the
following properties (which are all optional):
identifier
A unique identifier should be declared which identifies the event listener,
and orderings can be build upon the identifier. If this property is not
explicitly defined, the service name is automatically used instead.
before
This property allows a custom sorting of registered listeners. The listener
is then dispatched before the given listener. The value is the identifier of
another event listener. Also, multiple event identifiers can be entered here,
separated by a comma.
after
This property allows a custom sorting of registered listeners. The listener
is then dispatched after the given listener. The value is the identifier of
another event listener. Also, multiple event identifiers can be entered here,
separated by a comma.
event
The fully-qualified class name (FQCN) of the dispatched event, that the
listener wants to react to. It is recommended to not specify this property,
but to use the FQCN as type declaration of the argument within the dispatched method
(usually
__invoke(EventName $event)).
method
The method to be called. If this property is not given, the listener class
is treated as invokable, thus its
__invoke() method is called.
The PHP attribute is repeatable, which allows to register the same class
to listen for different events, for example:
If using the PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener
to configure an event listener, the registration in the
Configuration/Services.yaml file is not necessary anymore.
If an extension author wants to provide a custom event listener, an according
entry with the tag
event.listener can be added to the
Configuration/Services.yaml file of that extension.
EXT:my_extension/Configuration/Services.yaml
services:# Place here the default dependency injection configurationMyVendor\MyExtension\EventListener\NullMailer:tags:-name:event.listenermethod:handleEventidentifier:'my-extension/null-mailer'before:'someIdentifier, anotherIdentifier'
The tag name
event.listener identifies that a listener should be registered.
The custom PHP class
\MyVendor\MyExtension\EventListener\NullMailer
serves as the listener whose
handleEvent() method is called, once the
event is dispatched. The name of the listened event is specified as
a typed argument to that dispatch method.
handleEvent(\TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent $event) will
for example listen on the event BeforeMailerSentMessageEvent.
The
identifier is a common name, so
orderings can be built upon the identifier, the optional
before and
after attributes allow for custom sorting against the
identifier
of other listeners. If no
identifier is specified, the service name (usually
the fully-qualified class name of the listener) is automatically used.
If no attribute
method is given, the class is treated as invokable, thus
its
__invoke() method will be called:
EXT:my_extension/Configuration/Services.yaml
services:# Place here the default dependency injection configurationMyVendor\MyExtension\EventListener\NullMailer:tags:-name:event.listeneridentifier:'my-extension/null-mailer'before:'someIdentifier, anotherIdentifier'
Existing event listeners can be overridden by custom implementations. This can be
performed with both methods, either by using the PHP
#[AsEventListener]
attribute, or via EXT:my_extension/Configuration/Services.yaml.
For example, a third-party extension listens on the event
\TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent via the PHP attribute:
If you want to replace this event listener with your custom implementation, your extension can
achieve this by specifying the overridden identifier via the PHP attribute:
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\EventListener;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
// Important: Use the 'identifier' of the original event here to be replaced!#[AsEventListener( identifier: 'ext-some-extension/modify-hreflang',
after: 'typo3-seo/hreflangGenerator',
)]
final readonly classMySeoEventListener{
publicfunction__invoke(ModifyHrefLangTagsEvent $event): void{
// ... custom code which overrides the// original EXT:some-extension listener ...
}
}
Copied!
or via Services.yaml declaration:
EXT:my_extension/Configuration/Services.yaml
# Provide your custom event class:MyVendor\MyExtension\EventListener\Seo\HrefLangEventListener:tags:-name:event.listener# Use the same identifier of the extension that you override!identifier:'ext-some-extension/modify-hreflang'
Copied!
Note
When overriding an event listener, be sure to check whatever the
listener provided as changes to an event. Your own event listener
implementation is now responsible for any functionality, because
the original listener will no longer be executed.
Make sure that you set the identifier property to exactly
the string which the original implementation uses. If the identifier is not mentioned specifically
in the original implementation, the service name (when unspecified, the fully-qualified name of the event
listener class) is used. You can inspect that identifier in the
System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension
lowlevel), see Debugging event handling. In this example,
if
identifier: 'ext-some-extension/modify-hreflang' is not defined, the identifier
will be set to
identifier: 'SomeVendor\SomeExtension\Seo\HrefLangEventListener' and you could
use that identifier in your implementation.
Note
Overriding listeners requires your extension to declare a dependency on the
EXT:some_extension extension (through composer.json, or for non-Composer
mode ext_emconf.php).
This ensures a proper loading order, so your extension is processed after the extension you want
to override.
Best practices
When configuring listeners, it is recommended to add one listener class per
event type, and have it called via
__invoke().
When creating a new event PHP class, it is recommended to add an
Event suffix to the PHP class, and to move it into an appropriate
folder like Classes/Event/ to easily discover events provided by a
package. Be careful about the context that should be exposed.
The same applies to creating a new event listener PHP class: Add
an
Listener suffix to the PHP class, and move it to a folder
Classes/EventListener/.
Emitters (TYPO3 Core or extension authors) should always use
Dependency Injection to receive the event
dispatcher object as a constructor argument, where possible, by adding a
type declaration for
\Psr\EventDispatcher\EventDispatcherInterface .
A unique and descriptive
identifier should be used for event listeners.
Any kind of event provided by TYPO3 Core falls under TYPO3's Core API
deprecation policy, except for its constructor arguments, which may vary. Events
that should only be used within TYPO3 Core, are marked as
@internal, just
like other non-API parts of TYPO3. Events marked as
@internal should be
avoided whenever technically possible.
Debugging event handling
A complete list of all registered event listeners can be viewed in the the
module System > Configuration > Event Listeners (PSR-14). The
lowlevel system extension has to be installed for
this module to be available.
List of event listeners in the Configuration module
To debug all events that are actually dispatched during a frontend request you
can use the admin panel:
Go to Admin Panel > Debug > Events and see all dispatched events.
The adminpanel system extension has to be
installed for this module to be available.
List of dispatched events in the Admin Panel
Event list
The following list contains PSR-14 events
in the TYPO3 Core .
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent
gets triggered after the page in the backend is rendered and includes the
rendered page body. Listeners may overwrite the page string if desired.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/after-backend-page-render',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterBackendPageRenderEvent $event): void{
$content = $event->getContent() . ' I was here';
$event->setContent($content);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event triggers after a page has been rendered.
Listeners may update the page content string with a modified
version if appropriate.
getContent()
Returns
string
setContent(string $content)
param $content
the content
getView()
Returns
\TYPO3\CMS\Core\View\ViewInterface
AfterFormEnginePageInitializedEvent
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent
is available to listen for after the form engine has been
initialized (all data has been persisted).
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent
is available to listen for after the form engine has been
initialized (and all data has been persisted). It will be dispatched after
records and columns are collected in the
\TYPO3\CMS\Backend\Controller\Page\LocalizationController .
The event receives:
The default columns and columns list built by
LocalizationController
The list of records that were analyzed to create the columns manifest
The parameters received by the
LocalizationController
The event allows changes to:
the columns
the columns list
This allows third-party code to read or manipulate the "columns manifest" that
gets displayed in the translation modal when a user has clicked the
Translate button in the page module, by implementing a listener for
the event.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
This event triggers after the LocalizationController (AJAX) has
selected page columns to be translated. Allows third parties to
add to or change the columns and content elements withing those
columns which will be available for localization through the
"translate" modal in the page module.
getColumns()
Returns list of columns, indexed by column position number, value is label (either LLL: or hardcoded).
Returns
array
setColumns(array $columns)
param $columns
the columns
getColumnList()
Returns a list of integer column position numbers used in the BackendLayout.
Returns an array of records which were used when building the original column
manifest and column position numbers list.
Returns
array
getParameters()
Returns request parameters passed to LocalizationController.
Returns
array
AfterPagePreviewUriGeneratedEvent
New in version 12.0
This PSR-14 event replaces the
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass']
postProcess hook.
The
\TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent
is executed in
\TYPO3\CMS\Backend\Routing->buildUri(), after the preview
URI has been built - or set by an event listener to
BeforePagePreviewUriGeneratedEvent. It allows to overwrite the built
preview URI. However, this event does not feature the possibility to modify the
parameters, since this will not have any effect, as the preview URI is directly
returned after event dispatching and no further action is done by the
\TYPO3\CMS\Backend\Routing\PreviewUriBuilder .
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/modify-preview-uri',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterPagePreviewUriGeneratedEvent $event): void{
// Add custom fragment to built preview URI
$uri = $event->getPreviewUri();
$uri = $uri->withFragment('#customFragment');
$event->setPreviewUri($uri);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent
allows prepared page tree items to be modified.
It is dispatched in the
\TYPO3\CMS\Backend\Controller\Page\TreeController
class after the page tree items have been resolved and prepared. The event
provides the current PSR-7 request object as well as the page tree items. All
items contain the corresponding page record in the special
_page key.
Labels
New in version 13.1
Tree node labels can be defined which offer customizable color markings for
tree nodes. They also require an associated label for improved
accessibility. Each node can support multiple labels, sorted by priority, with
the highest priority label taking precedence over others.
A label can also be assigned to a node via
user TSconfig. Please note that
only the marker for the label with the highest priority is rendered. All
additional labels will only be added to the title of the node.
Status information
New in version 13.1
Tree nodes can incorporate status information. These details serve to indicate
the status of nodes and provide supplementary information. For instance, if a
page undergoes changes within a workspace, it will display an indicator on
the respective tree node. Additionally, the status is appended to the node's
title. This not only improves visual clarity but also enhances information
accessibility.
Each node can accommodate multiple status information, prioritized by severity
and urgency. Critical messages take precedence over other status notifications.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent;
useTYPO3\CMS\Backend\Dto\Tree\Label\Label;
useTYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
#[AsEventListener(
identifier: 'my-extension/backend/modify-page-tree-items',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterPageTreeItemsPreparedEvent $event): void{
$items = $event->getItems();
foreach ($items as &$item) {
if (($item['_page']['pid'] ?? null) === 123) {
// Set special icon for page with ID 123
$item['icon'] = 'my-special-icon';
// Set a tree node label
$item['labels'][] = new Label(
label: 'Campaign B',
color: '#00658f',
priority: 1,
);
// Set a status information
$item['statusInformation'][] = new StatusInformation(
label: 'A warning message',
severity: ContextualFeedbackSeverity::WARNING,
priority: 0,
icon: 'actions-dot',
overlayIcon: '',
);
}
}
$event->setItems($items);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this event will be able to modify the prepared page tree items for the page tree
getRequest()
Returns
\Psr\Http\Message\ServerRequestInterface
getItems()
Returns
array
setItems(array $items)
param $items
the items
AfterRawPageRowPreparedEvent
New in version 13.3
This event was introduced to replace the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections']
which has already been removed with TYPO3 v9.
The PSR-14 event
\TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent
allows to modify the populated properties of a page and children records before
the page is displayed in a page tree.
This can be used, for example, to change the title of a page or apply a
different sorting to its children.
Listeners to this event will be able to modify a page with the special _children key,
or completely change e.g. a title.
getRawPage()
Returns
array
setRawPage(array $rawPage)
param $rawPage
the rawPage
getWorkspaceId()
Returns
int
AfterRecordSummaryForLocalizationEvent
New in version 12.0
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent
is fired in the
\TYPO3\CMS\Backend\Controller\Page\RecordSummaryForLocalization class
and allows extensions to modify the payload of the
JsonResponse.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/after-record-summary-for-localization',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterRecordSummaryForLocalizationEvent $event): void{
// Get current records
$records = $event->getRecords();
// ... do something with $records// Set new records
$event->setRecords($records);
// Get current columns
$columns = $event->getColumns();
// ... do something with $columns// Set new columns
$event->setColumns($columns);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The PSR-14 event
\TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent
allows extension authors to display content in any colPos after the last
content element.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/after-section-markup-generated',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterSectionMarkupGeneratedEvent $event): void{
// Check for relevant backend layoutif ($event->getPageLayoutContext()->getBackendLayout()->getIdentifier() !== 'someBackendLayout') {
return;
}
// Check for relevant columnif ($event->getColumnConfig()['identifier'] !== 'someColumn') {
return;
}
$event->setContent('
<div class="t3-page-ce">
<div class="t3-page-ce-element">
<div class="t3-page-ce-header">
<div class="t3-page-ce-header-title">
Some content at the end of the column
</div>
</div>
</div>
</div>
');
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event can be triggered to display content in any colpos after
the last content element.
getColumnConfig()
Returns
array
getPageLayoutContext()
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getRecords()
Returns
array
setContent(string $content = '')
param $content
the content, default: ''
getContent()
Returns
string
isPropagationStopped()
Prevent other listeners from being called if rendering is stopped by listener.
Returns
bool
setStopRendering(bool $stopRendering)
param $stopRendering
the stopRendering
BeforeFormEnginePageInitializedEvent
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent
allows to listen for before the form engine has been
initialized (before all data will be persisted).
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
The PSR-14 event
\TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent
allows extension authors to manipulate the module configuration, before it is used to create and register the
module.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This PSR-14 event replaces the
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass']
preProcess hook.
The
\TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent
is executed in
\TYPO3\CMS\Backend\Routing->buildUri(), before the preview
URI is actually built. It allows to either adjust the parameters, such as the
page id or the language id, or to set a custom preview URI, which will then stop
the event propagation and also prevents
\TYPO3\CMS\Backend\Routing\PreviewUriBuilder from building the URI based
on the parameters.
Note
The overwritten parameters are used for building the URI and are also passed
to the AfterPagePreviewUriGeneratedEvent. They however do not
overwrite the related class properties in
PreviewUriBuilder.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this event will be able to modify the corresponding parameters, before
the page preview URI is being generated, when linking to a page in the frontend.
This PSR-14 event replaces the
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader']
and
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] ,
hooks, which have been deprecated with TYPO3 v13.2. See also
Migration.
The event
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent
can be used to modify the result of a download / export initiated via
the Web > List module.
The event lets you change both the main part and the header of the data file.
You can use it to edit data to follow GDPR rules, change or translate data,
create backups or web hooks, record who accesses the data, and more.
Listeners to this event are able to manipulate the download of records, usually triggered via Web > List.
getHeaderRow()
Returns
array
setHeaderRow(array $headerRow)
param $headerRow
the headerRow
getRecords()
Returns
array
setRecords(array $records)
param $records
the records
getRequest()
Returns
\Psr\Http\Message\ServerRequestInterface
getTable()
Returns
string
getFormat()
Returns
string
getFilename()
Returns
string
getId()
Returns
int
getModTSconfig()
Returns
array
getColumnsToRender()
Returns
array
isHideTranslations()
Returns
bool
Migration
Deprecated since version 13.2
The previously used hooks
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader']
and
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] ,
used to manipulate the download / export configuration of records, triggered
in the Web > List backend module, have been deprecated in favor of a
new PSR-14 event
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent .
Migrating
customizeCsvHeader
The prior hook parameter/variable
fields is now available via
$event->getColumnsToRender(). The actual record data
(previously
$this->recordList, submitted to the hook as its object
reference) is accessible via
$event->getHeaderRow().
Migrating
customizeCsvRow
The following prior hook parameters/variables have these substitutes:
databaseRow
is now available via
$event->getRecords() (see note below).
tableName
is now available via
$event->getTable().
pageId
is now available via
$event->getId().
The actual record data
(previously
$this->recordList, submitted to the hook as its object
reference) is accessible via
$event->getRecords().
Please note that the hook was previously executed once per row retrieved
from the database. The PSR-14 event however - due to performance reasons -
is only executed for the full record list after database retrieval,
thus allows post-processing on this whole dataset.
BeforeRecordDownloadPresetsAreDisplayedEvent
New in version 13.2
The event
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent
can be used to manipulate the list of available download presets in
the Web > List module.
Note that the event is dispatched for one specific database table name. If
an event listener is created to attach presets to different tables, the
listener method must check for the table name, as shown in the example below.
If no download presets exist for a given table, the PSR-14 event can still
be used to modify and add presets to it via the
setPresets() method.
The array passed from
getPresets() to
setPresets() can contain
an array collection of TYPO3CMSBackendRecordListDownloadPreset
objects with the array key using the preset label.
The event listener can also remove array indexes, or also columns of existing
array entries by passing a newly constructed
DownloadPreset object with the
changed label and columns constructor properties.
Event to manipulate the available list of download presets.
Array $presets contains a list of DownloadPreset objects
with their methods: getIdentifier(), getLabel() and getColumns().
The event is always coupled to a specific database table.
getPresets()
Returns
\DownloadPreset[]
setPresets(array $presets)
param $presets
the presets
getDatabaseTable()
Returns
string
getRequest()
Returns
\Psr\Http\Message\ServerRequestInterface
getId()
Returns
int
API of DownloadPreset
classDownloadPreset
Fully qualified name
\TYPO3\CMS\Backend\RecordList\DownloadPreset
getIdentifier()
Returns
string
getLabel()
Returns
string
getColumns()
Returns
array
create(array $preset)
param $preset
the preset
Returns
self
BeforeSearchInDatabaseRecordProviderEvent
New in version 12.1
The TYPO3 backend search (also known as "Live Search") uses the
\TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider to search
for records in database tables, having
searchFields configured in TCA.
In some individual cases it may not be desirable to search in a specific table.
Therefore, the PSR-14 event
\TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent
is available, which allows to exclude / ignore such tables by adding them
to a deny list. Additionally, the PSR-14 event can be used to restrict the
search result on certain page IDs or to modify the search query altogether.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
PSR-14 event to modify the incoming input about which tables should be searched for within
the live search results. This allows adding additional DB tables to be excluded / ignored, to
further limit the search result on certain page IDs or to modify the search query altogether.
The PSR-14 event
\TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent
allows extension authors to display content in any colPos before the first
content element.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/before-section-markup-generated',
)]
final readonly classMyEventListener{
publicfunction__invoke(BeforeSectionMarkupGeneratedEvent $event): void{
// Check for relevant backend layoutif ($event->getPageLayoutContext()->getBackendLayout()->getIdentifier() !== 'someBackendLayout') {
return;
}
// Check for relevant columnif ($event->getColumnConfig()['identifier'] !== 'someColumn') {
return;
}
$event->setContent('
<div class="t3-page-ce">
<div class="t3-page-ce-element">
<div class="t3-page-ce-header">
<div class="t3-page-ce-header-title">
Some content at the start of the column
</div>
</div>
</div>
</div>
');
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event can be triggered to display content in any colPos
before the first content element.
getColumnConfig()
Returns
array
getPageLayoutContext()
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getRecords()
Returns
array
setContent(string $content = '')
param $content
the content, default: ''
getContent()
Returns
string
isPropagationStopped()
Prevent other listeners from being called if rendering is stopped by listener.
Returns
bool
setStopRendering(bool $stopRendering)
param $stopRendering
the stopRendering
CustomFileControlsEvent
New in version 12.0
This event replaces the
customControls
hook option, which is only available for TCA type
inline.
Listeners to the PSR-14 event
\TYPO3\CMS\Backend\Form\Event\CustomFileControlsEvent
are able to add custom controls to a TCA type
file field in form engine.
Custom controls are always displayed below the file references. In contrast
to the selectors, e.g. Select & upload files are custom controls
independent of the
readonly and
showFileSelectors options.
This means, you have full control in which scenario your custom controls
are being displayed.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
This event
\TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent
serves as a drop-in replacement for the removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used']
hook.
Use the PSR-14 event
\TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent
to identify if content has been used in a column that is not in a backend layout.
Setting
$event->setUsed(true) prevent the following message for the affected content element,
setting it to false displays it:
Warning
Unused elements detected on this page
These elements don't belong to any of the available columns of this page. You should either delete
them or move them to existing columns. We highlighted the problematic records for you.
Example: Display "Unused elements detected on this page" for elements with missing parent
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Listener;
useTYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/view/content-used-on-page',
)]
final readonly classContentUsedOnPage{
publicfunction__invoke(IsContentUsedOnPageLayoutEvent $event): void{
// Get the current record from the event.
$record = $event->getRecord();
// This code will be your domain logic to indicate if content// should be hidden in the page module.if ((int)($record['colPos'] ?? 0) === 999
&& !empty($record['tx_myext_content_parent'])
) {
// Flag the current element as not used. Set it to true, if you// want to flag it as used and hide it from the page module.
$event->setUsed(false);
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this event are able to define whether a file can be selected in the file browser
getFile()
Returns
\TYPO3\CMS\Core\Resource\FileInterface
isFileSelectable()
Returns
bool
allowFileSelection()
denyFileSelection()
ModifyAllowedItemsEvent
New in version 12.0
This event has been introduced together with
ModifyLinkHandlersEvent to
serve as a direct replacement for the following removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] .
It replaces the method
modifyAllowedItems() in this hook.
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent
allows extension authors to add or remove from the list of allowed link
types.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event allows extensions to add or remove from the list of allowed link types.
getAllowedItems()
Returns
string[]
addAllowedItem(string $item)
param $item
the item
Returns
self
removeAllowedItem(string $new)
param $new
the new
Returns
self
getCurrentLinkParts()
Returns
array<string,mixed>
ModifyButtonBarEvent
New in version 12.0
This event serves as a direct replacement for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend\Template\Components\ButtonBar']['getButtonsHook'] .
The PSR-14 event
\TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent
can be used to modify the button bar in the TYPO3 backend module
docheader.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/modify-button-bar',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyButtonBarEvent $event): void{
// Do your magic here
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners can modify the buttons of the button bar in the backend module docheader
getButtons()
Returns
\Buttons
setButtons(array $buttons)
param $buttons
the buttons
getButtonBar()
Returns
\TYPO3\CMS\Backend\Template\Components\ButtonBar
ModifyClearCacheActionsEvent
The PSR-14 event
\TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent
is fired in the
\TYPO3\CMS\Backend\Backend\ToolbarItems\ClearCacheToolbarItem
class and allows extension authors to modify the clear cache actions, shown
in the TYPO3 backend top toolbar.
The event can be used to change or remove existing clear cache
actions, as well as to add new actions. Therefore, the event also
contains, next to the usual "getter" and "setter" methods, the convenience
method
add() for the
cacheActions and
cacheActionIdentifiers arrays.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/toolbar/my-event-listener',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyClearCacheActionsEvent $event): void{
// do magic here
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The cache action array element consists of the following keys and values:
The key
severity can contain one of these strings: notice, info, success, warning, error.
The cache identifier array is a numerical array in which the array value corresponds to the registered id of the cache action array.
Here is an example of how to use it for a custom cache action:
Example cache action array combined with a cache identifier array
$myIdentifier = 'myExtensionCustomStorageCache';
$event->addCacheAction([
'id' => $myIdentifier,
'title' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:CacheActionTitle',
// Note to register your own route, this is an example'href' => (string)$uriBuilder->buildUriFromRoute('ajax_' . $myIdentifier . '_purge'),
'iconIdentifier' => 'actions-system-cache-clear-impact-low',
'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:CacheActionDescription',
'severity' => 'notice',
]);
$event->addCacheActionIdentifier($myIdentifier);
This event has been introduced which serves as a drop-in replacement for the removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery']
hook.
Use the PSR-14 event
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent
to filter out certain content elements from being shown in the
Page module.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Database\Connection;
#[AsEventListener(
identifier: 'my-extension/backend/modify-database-query-for-content',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyDatabaseQueryForContentEvent $event): void{
// Early return if we do not need to reactif ($event->getTable() !== 'tt_content') {
return;
}
// Retrieve QueryBuilder instance from event
$queryBuilder = $event->getQueryBuilder();
// Add an additional condition to the QueryBuilder for the table// Note: This is only a example, modify the QueryBuilder instance// here to your needs.
$queryBuilder = $queryBuilder->andWhere(
$queryBuilder->expr()->neq(
'some_field',
$queryBuilder->createNamedParameter(1, Connection::PARAM_INT),
),
);
// Set updated QueryBuilder to event
$event->setQueryBuilder($queryBuilder);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The PSR-14 event
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent
allows to alter the query builder SQL
statement before a list of records is rendered in record lists, such as
the List module or an element browser.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event serves as a more powerful and flexible alternative for the removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/alt_doc.php']['makeEditForm_accessCheck']
hook.
The PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent\ModifyEditFormUserAccessEvent
provides the full database row of the record in question next to the
exception, which might have been set by the Core. Additionally, the event allows
to modify the user access decision in an object-oriented way, using
convenience methods.
In case any listener to the new event denies user access, while it was initially
allowed by Core, the
\TYPO3\CMS\Backend\Form\Exception\AccessDeniedListenerException
will be thrown.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/modify-edit-form-user-access',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyEditFormUserAccessEvent $event): void{
// Deny access for creating records of a custom tableif ($event->getTableName() === 'tx_myext_domain_model_mytable' && $event->getCommand() === 'new') {
$event->denyUserAccess();
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to the PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceControlsEvent
are able to modify the controls of a single file reference of a TCA type
file field. This event is similar to the
ModifyInlineElementControlsEvent, which is only available for TCA
type inline.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
Listeners to this Event will be able to modify the controls
of a single file reference of a TCA type=file field.
getControls()
Returns all controls with their markup
Returns
array
setControls(array $controls)
Overwrite the controls
param $controls
the controls
getControl(string $identifier)
Returns the markup for the requested control
param $identifier
the identifier
Returns
string
setControl(string $identifier, string $markup)
Set a control with the given identifier and markup
IMPORTANT: Overwrites an existing control with the same identifier
param $identifier
the identifier
param $markup
the markup
hasControl(string $identifier)
Returns whether a control exists for the given identifier
param $identifier
the identifier
Returns
bool
removeControl(string $identifier)
Removes a control from the file reference, if it exists
param $identifier
the identifier
Return description
Whether the control could be removed
Returns
bool
getElementData()
Returns the whole element data
Returns
array
getRecord()
Returns the current record, the controls are created for
Returns
array
getParentUid()
Returns the uid of the parent (embedding) record (uid or NEW...)
Returns
string
getForeignTable()
Returns the table (foreign_table) the controls are created for
Returns
string
getFieldConfiguration()
Returns the TCA configuration of the TCA type=file field
Returns
array
isVirtual()
Returns whether the current records is only virtually shown and not physically part of the parent record
Returns
bool
ModifyFileReferenceEnabledControlsEvent
New in version 12.0
Listeners to the PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceEnabledControlsEvent
are able to modify the state (enabled or disabled) for the controls of a single
file reference of a TCA type file field. This
event is similar to the ModifyInlineElementEnabledControlsEvent, which
is only available for TCA type inline.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
Listeners to this Event will be able to modify the state (enabled or disabled) for controls of a file reference
enableControl(string $identifier)
Enable a control, if it exists
param $identifier
the identifier
Return description
Whether the control could be enabled
Returns
bool
disableControl(string $identifier)
Disable a control, if it exists
param $identifier
the identifier
Return description
Whether the control could be disabled
Returns
bool
hasControl(string $identifier)
Returns whether a control exists for the given identifier
param $identifier
the identifier
Returns
bool
isControlEnabled(string $identifier)
Returns whether the control is enabled.
Note: Will also return FALSE in case no control exists for the requested identifier
param $identifier
the identifier
Returns
bool
getControlsState()
Returns all controls with their state (enabled or disabled)
Returns
array
getEnabledControls()
Returns all enabled controls
Returns
array
getElementData()
Returns the whole element data
Returns
array
getRecord()
Returns the current record of the controls are created for
Returns
array
getParentUid()
Returns the uid of the parent (embedding) record (uid or NEW...)
Returns
string
getForeignTable()
Returns the table (foreign_table) the controls are created for
Returns
string
getFieldConfiguration()
Returns the TCA configuration of the TCA type=file field
Returns
array
isVirtual()
Returns whether the current records is only virtually shown and not physically part of the parent record
Returns
bool
ModifyGenericBackendMessagesEvent
New in version 12.0
This event serves as direct replacement for the now removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['displayWarningMessages'] .
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent
allows to add or alter messages that are displayed in the About
module (default start module of the TYPO3 backend).
Extensions such as the EXT:reports system extension
use this event to display custom messages based on the system status:
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event serves as a direct replacement for the now removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend/Form/Element/ImageManipulationElement']['previewUrl']
hook.
The PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent
can be used to modify the preview URL within the
image manipulation element, used
for example for the
crop field of the
sys_file_reference table.
As soon as a preview URL is set, the image manipulation element will display
a corresponding button in the footer of the modal window, next to the
Cancel and Accept buttons. On click, the preview
URL will be opened in a new window.
Note
The element's crop variants will always be appended to the preview URL
as JSON-encoded string, using the cropVariants parameter.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this Event will be able to modify the preview url, used in the ImageManipulation element
getDatabaseRow()
Returns
array
getFieldConfiguration()
Returns
array
getFile()
Returns
\TYPO3\CMS\Core\Resource\File
getPreviewUrl()
Returns
string
setPreviewUrl(string $previewUrl)
param $previewUrl
the previewUrl
ModifyInlineElementControlsEvent
New in version 12.0
This event, together with ModifyInlineElementEnabledControlsEvent,
serves as a more powerful and flexible replacement
for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook']
The PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent
is called after the markup for all enabled controls has been generated. It
can be used to either change the markup of a control, to add a new control
or to completely remove a control.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent;
useTYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Imaging\IconFactory;
useTYPO3\CMS\Core\Imaging\IconSize;
useTYPO3\CMS\Core\Utility\GeneralUtility;
#[AsEventListener(
identifier: 'my-extension/backend/modify-enabled-controls',
method: 'modifyEnabledControls',
)]
#[AsEventListener(
identifier: 'my-extension/backend/modify-controls',
method: 'modifyControls',
)]
final readonly classMyEventListener{
publicfunctionmodifyEnabledControls(ModifyInlineElementEnabledControlsEvent $event): void{
// Enable a control depending on the foreign tableif ($event->getForeignTable() === 'sys_file_reference' && $event->isControlEnabled('sort')) {
$event->enableControl('sort');
}
}
publicfunctionmodifyControls(ModifyInlineElementControlsEvent $event): void{
// Add a custom control depending on the parent tableif ($event->getElementData()['inlineParentTableName'] === 'tt_content') {
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$event->setControl(
'tx_my_control',
'<a href="/some/url" class="btn btn-default t3js-modal-trigger">'
. $iconFactory->getIcon('my-icon-identifier', IconSize::SMALL)->render()
. '</a>',
);
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this Event will be able to modify the controls of an inline element
getControls()
Returns all controls with their markup
Returns
array
setControls(array $controls)
Overwrite the controls
param $controls
the controls
getControl(string $identifier)
Returns the markup for the requested control
param $identifier
the identifier
Returns
string
setControl(string $identifier, string $markup)
Set a control with the given identifier and markup
IMPORTANT: Overwrites an existing control with the same identifier
param $identifier
the identifier
param $markup
the markup
hasControl(string $identifier)
Returns whether a control exists for the given identifier
param $identifier
the identifier
Returns
bool
removeControl(string $identifier)
Removes a control from the inline element, if it exists
param $identifier
the identifier
Return description
Whether the control could be removed
Returns
bool
getElementData()
Returns the whole element data
Returns
array
getRecord()
Returns the current record of the controls are created for
Returns
array
getParentUid()
Returns the uid of the parent (embedding) record (uid or NEW...)
Returns
string
getForeignTable()
Returns the table (foreign_table) the controls are created for
Returns
string
getFieldConfiguration()
Returns the TCA configuration of the inline record field
Returns
array
isVirtual()
Returns whether the current records is only virtually shown and not physically part of the parent record
Returns
bool
ModifyInlineElementEnabledControlsEvent
New in version 12.0
This event, together with ModifyInlineElementControlsEvent,
serves as a more powerful and flexible replacement
for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook']
The PSR-14 event
\TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent
is called before any control markup is generated. It can be used to
enable or disable each control. With this event it is therefore possible
to enable a control, which is disabled in TCA, only for some use case.
Listeners to this Event will be able to modify the state (enabled or disabled) for controls of an inline element
enableControl(string $identifier)
Enable a control, if it exists
param $identifier
the identifier
Return description
Whether the control could be enabled
Returns
bool
disableControl(string $identifier)
Disable a control, if it exists
param $identifier
the identifier
Return description
Whether the control could be disabled
Returns
bool
hasControl(string $identifier)
Returns whether a control exists for the given identifier
param $identifier
the identifier
Returns
bool
isControlEnabled(string $identifier)
Returns whether the control is enabled.
Note: Will also return FALSE in case no control exists for the requested identifier
param $identifier
the identifier
Returns
bool
getControlsState()
Returns all controls with their state (enabled or disabled)
Returns
array
getEnabledControls()
Returns all enabled controls
Returns
array
getElementData()
Returns the whole element data
Returns
array
getRecord()
Returns the current record of the controls are created for
Returns
array
getParentUid()
Returns the uid of the parent (embedding) record (uid or NEW...)
Returns
string
getForeignTable()
Returns the table (foreign_table) the controls are created for
Returns
string
getFieldConfiguration()
Returns the TCA configuration of the inline record field
Returns
array
isVirtual()
Returns whether the current records is only virtually shown and not physically part of the parent record
Returns
bool
ModifyLinkExplanationEvent
New in version 12.0
This event serves as a more powerful and flexible alternative
for the removed
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler']
hook.
While the removed hook effectively only allowed to modify the link explanation
of TCA link fields in case the resolved link type did not already match
one of those, implemented by TYPO3 itself, the new event allows to
always modify the link explanation of any type. Additionally, this also allows
to modify the additionalAttributes, displayed below the actual link
explanation field. This is especially useful for extended link handler setups.
To modify the link explanation, the following methods are available:
getLinkExplanation(): Returns the current link explanation data
setLinkExplanation(): Set the link explanation data
getLinkExplanationValue(): Returns a specific link explanation value
setLinkExplanationValue(): Sets a specific link explanation value
The link explanation array usually contains the following values:
text : The text to show in the link explanation field
icon: The markup for the icon, displayed in front of the link explanation field
additionalAttributes: The markup for additional attributes, displayed below the link explanation field
The current context can be evaluated using the following methods:
getLinkData(): Returns the resolved link data, such as the page uid
getLinkParts(): Returns the resolved link parts, such as url, target and additionalParams
getElementData(): Returns the full FormEngine $data array for the current element
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Imaging\IconFactory;
useTYPO3\CMS\Core\Imaging\IconSize;
#[AsEventListener(
identifier: 'my-extension/backend/modify-link-explanation',
)]
final readonly classMyEventListener{
publicfunction__construct(
private IconFactory $iconFactory,
){}
publicfunction__invoke(ModifyLinkExplanationEvent $event): void{
// Use a custom icon for a custom link typeif ($event->getLinkData()['type'] === 'myCustomLinkType') {
$event->setLinkExplanationValue(
'icon',
$this->iconFactory->getIcon(
'my-custom-link-icon',
IconSize::SMALL,
)->render(),
);
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event has been introduced together with
ModifyAllowedItemsEvent to
serve as a direct replacement for the following removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] .
It replaces the method
modifyLinkHandlers() in this hook.
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent
is triggered before link handlers are executed, allowing listeners
to modify the set of handlers that will be used.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event allows extensions to modify the list of link handlers and their configuration before they are invoked.
getLinkHandlers()
Returns
array<string,array>
getLinkHandler(string $name)
Gets an individual handler by name.
param $name
The handler name, including trailing period.
Return description
The handler definition, or null if not defined.
Returns
array<string,mixed>|null
setLinkHandler(string $name, array $handler)
Sets a handler by name, overwriting it if it already exists.
param $name
The handler name, including trailing period.
param $handler
the handler
Returns
$this
getCurrentLinkParts()
Returns
array<string,mixed>
ModifyNewContentElementWizardItemsEvent
New in version 12.0
This event serves as a more powerful and flexible alternative
for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'] .
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent
is called after TYPO3 has already prepared the wizard items,
defined in page TSconfig (mod.wizards.newContentElement.wizardItems).
The event allows listeners to modify any available wizard item as well
as adding new ones. It is therefore possible for the listeners to, for example,
change the configuration, the position or to remove existing items altogether.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/backend/modify-page-module-content',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyPageLayoutContentEvent $event): void{
// Get the current page ID
$id = (int)($event->getRequest()->getQueryParams()['id'] ?? 0);
$event->addHeaderContent('Additional header content');
$event->setFooterContent('Overwrite footer content');
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this Event will be able to modify the header and footer content of the page module
getRequest()
Returns
\Psr\Http\Message\ServerRequestInterface
getModuleTemplate()
Returns
\TYPO3\CMS\Backend\Template\ModuleTemplate
setHeaderContent(string $content)
Set content for the header. Can also be used to e.g. reorder existing content.
IMPORTANT: This overwrites existing content from previous listeners!
param $content
the content
addHeaderContent(string $content)
Add additional content to the header
param $content
the content
getHeaderContent()
Returns
string
setFooterContent(string $content)
Set content for the footer. Can also be used to e.g. reorder existing content.
IMPORTANT: This overwrites existing content from previous listeners!
param $content
the content
addFooterContent(string $content)
Add additional content to the footer
param $content
the content
getFooterContent()
Returns
string
ModifyPageLayoutOnLoginProviderSelectionEvent
The PSR-14 event
\TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent
allows to modify variables for the view depending on a special login provider
set in the controller.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
The PSR-14 event
\TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent
can be used to modify the live search queries in the backend.
This can be used to adjust the limit for a specific table or to change the
result order.
This event is fired in the
\TYPO3\CMS\Backend\Search\LiveSearch\LiveSearch class
and allows extensions to modify the query builder
instance before execution.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/modify-query-for-live-search-event-listener',
)]
final readonly classMyEventListener{
publicfunction__invoke(ModifyQueryForLiveSearchEvent $event): void{
// Get the current instance
$queryBuilder = $event->getQueryBuilder();
// Change limit depending on the tableif ($event->getTableName() === 'pages') {
$queryBuilder->setMaxResults(2);
}
// Reset the orderBy part
$queryBuilder->resetQueryPart('orderBy');
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
PSR-14 event to modify the query builder instance for the live search
getQueryBuilder()
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
getTableName()
Returns
string
ModifyRecordListHeaderColumnsEvent
New in version 11.4
Changed in version 12.0
Due to the integration of EXT:recordlist into EXT:backend the namespace of
the event changed from
\TYPO3\CMS\Recordlist\Event\ModifyRecordListHeaderColumnsEvent
to
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent .
For TYPO3 v12 the moved class is available as an alias under the old
namespace to allow extensions to be compatible with TYPO3 v11 and v12.
The PSR-14 event
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent
allows to modify the header columns for a table in the record list.
Add a new column or override an existing one. Latter is only possible,
in case $columnName is given. Otherwise, the column will be added with
a numeric index, which is generally not recommended.
Note: Due to the behaviour of DatabaseRecordList, just adding a column
does not mean that it is also displayed. The internal $fieldArray needs
to be adjusted as well. This method only adds the column to the data array.
Therefore, this method should mainly be used to edit existing columns, e.g.
change their label.
param $column
the column
param $columnName
the columnName, default: ''
hasColumn(string $columnName)
Whether the column exists
param $columnName
the columnName
Returns
bool
getColumn(string $columnName)
Get column by its name
param $columnName
the columnName
Return description
The column or NULL if the column does not exist
Returns
string|null
removeColumn(string $columnName)
Remove column by its name
param $columnName
the columnName
Return description
Whether the column could be removed - Will thereforereturn FALSE if the column to remove does not exist.
Returns
bool
setColumns(array $columns)
param $columns
the columns
getColumns()
Returns
array
setHeaderAttributes(array $headerAttributes)
param $headerAttributes
the headerAttributes
getHeaderAttributes()
Returns
array
getTable()
Returns
string
getRecordIds()
Returns
array
getRecordList()
Returns the current DatabaseRecordList instance.
Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList
ModifyRecordListRecordActionsEvent
New in version 11.4
Changed in version 12.0
Due to the integration of EXT:recordlist into EXT:backend the namespace of
the event changed from
\TYPO3\CMS\Recordlist\Event\ModifyRecordListRecordActionsEvent
to
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent .
For TYPO3 v12 the moved class is available as an alias under the old
namespace to allow extensions to be compatible with TYPO3 v11 and v12.
The PSR-14 event
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent
allows to modify the displayed record actions (for example
edit, copy, delete) for a table in
the record list.
Add a new action or override an existing one. Latter is only possible,
in case $columnName is given. Otherwise, the column will be added with
a numeric index, which is generally not recommended. It's also possible
to define the position of an action with either the "before" or "after"
argument, while their value must be an existing action.
Note: In case non or an invalid $group is provided, the new action will
be added to the secondary group.
param $action
the action
param $actionName
the actionName, default: ''
param $group
the group, default: ''
param $before
the before, default: ''
param $after
the after, default: ''
hasAction(string $actionName, string $group = '')
Whether the action exists in the given group. In case non or
an invalid $group is provided, both groups will be checked.
param $actionName
the actionName
param $group
the group, default: ''
Returns
bool
getAction(string $actionName, string $group = '')
Get action by its name. In case the action exists in both groups
and non or an invalid $group is provided, the action from the
"primary" group will be returned.
Remove action by its name. In case the action exists in both groups
and non or an invalid $group is provided, the action will be removed
from both groups.
param $actionName
the actionName
param $group
the group, default: ''
Return description
Whether the action could be removed - Will thereforereturn FALSE if the action to remove does not exist.
Returns
bool
getActionGroup(string $group)
Get the actions of a specific group
param $group
the group
Returns
?array
setActions(array $actions)
param $actions
the actions
getActions()
Returns
array
getTable()
Returns
string
getRecord()
Returns
array
getRecordList()
Returns the current DatabaseRecordList instance.
Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList
ModifyRecordListTableActionsEvent
New in version 11.4
Changed in version 12.0
Due to the integration of EXT:recordlist into EXT:backend the namespace of
the event changed from
\TYPO3\CMS\Recordlist\Event\ModifyRecordListTableActionsEvent
to
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent .
For TYPO3 v12 the moved class is available as an alias under the old
namespace to allow extensions to be compatible with TYPO3 v11 and v12.
The PSR-14 event
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent
allows to modify the multi record selection actions (for example
edit, copy to clipboard) for a table in the record list.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
usePsr\Log\LoggerInterface;
useTYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent;
useTYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
useTYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/recordlist/my-event-listener',
method: 'modifyRecordActions',
)]
#[AsEventListener(
identifier: 'my-extension/recordlist/my-event-listener',
method: 'modifyHeaderColumns',
)]
#[AsEventListener(
identifier: 'my-extension/recordlist/my-event-listener',
method: 'modifyTableActions',
)]
final readonly classMyEventListener{
publicfunction__construct(
private LoggerInterface $logger,
){}
publicfunctionmodifyRecordActions(ModifyRecordListRecordActionsEvent $event): void{
$currentTable = $event->getTable();
// Add a custom action for a custom table in the secondary action bar, before the "move" actionif ($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 groupif (count($event->getActionGroup('secondary')) > 4 && $event->hasAction('viewBig')) {
$event->removeAction('viewBig');
}
// Move the "delete" action after the "edit" action
$event->setAction('', 'delete', 'primary', '', 'edit');
}
publicfunctionmodifyHeaderColumns(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']);
}
publicfunctionmodifyTableActions(ModifyRecordListTableActionsEvent $event): void{
// Remove "edit" action and log, if this failed
$actionRemoved = $event->removeAction('unknown');
if (!$actionRemoved) {
$this->logger->warning('Action "unknown" could not be removed');
}
// Add a custom clipboard action after "copyMarked"
$event->setAction('<button>My action</button>', 'myAction', '', 'copyMarked');
// Set a custom label for the case, no actions are available for the user
$event->setNoActionLabel('No actions available due to missing permissions.');
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Add a new action or override an existing one. Latter is only possible,
in case $actionName is given. Otherwise, the action will be added with
a numeric index, which is generally not recommended. It's also possible
to define the position of an action with either the "before" or "after"
argument, while their value must be an existing action.
param $action
the action
param $actionName
the actionName, default: ''
param $before
the before, default: ''
param $after
the after, default: ''
hasAction(string $actionName)
Whether the action exists
param $actionName
the actionName
Returns
bool
getAction(string $actionName)
Get action by its name
param $actionName
the actionName
Return description
The action or NULL if the action does not exist
Returns
string|null
removeAction(string $actionName)
Remove action by its name
param $actionName
the actionName
Return description
Whether the action could be removed - Will thereforereturn FALSE if the action to remove does not exist.
Returns
bool
setActions(array $actions)
param $actions
the actions
getActions()
Returns
array
setNoActionLabel(string $noActionLabel)
param $noActionLabel
the noActionLabel
getNoActionLabel()
Get the label, which will be displayed, in case no
action is available for the current user. Note: If
this returns an empty string, this only means that
no other listener set a label before. TYPO3 will
always fall back to a default if this remains empty.
Returns
string
getTable()
Returns
string
getRecordIds()
Returns
array
getRecordList()
Returns the current DatabaseRecordList instance.
Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList
ModifyResultItemInLiveSearchEvent
New in version 12.2
The PSR-14 event
\TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent
allows extension developers to take control over search result
items rendered in the backend search.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\Routing\UriBuilder;
useTYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent;
useTYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider;
useTYPO3\CMS\Backend\Search\LiveSearch\ResultItemAction;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Imaging\IconFactory;
useTYPO3\CMS\Core\Imaging\IconSize;
useTYPO3\CMS\Core\Localization\LanguageService;
useTYPO3\CMS\Core\Localization\LanguageServiceFactory;
#[AsEventListener(
identifier: 'my-extension/add-live-search-result-actions-listener',
)]
final readonly classMyEventListener{
private LanguageService $languageService;
publicfunction__construct(
private IconFactory $iconFactory,
LanguageServiceFactory $languageServiceFactory,
private UriBuilder $uriBuilder,
){
$this->languageService = $languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
}
publicfunction__invoke(ModifyResultItemInLiveSearchEvent $event): void{
$resultItem = $event->getResultItem();
if ($resultItem->getProviderClassName() !== DatabaseRecordProvider::class) {
return;
}
if (($resultItem->getExtraData()['table'] ?? null) === 'tt_content') {
/**
* WARNING: THIS EXAMPLE OMITS ANY ACCESS CHECK FOR SIMPLICITY REASONS - DO NOT USE AS-IS
*/
$showHistoryAction = (new ResultItemAction('view_history'))
->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:history'))
->setIcon($this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL))
->setUrl((string)$this->uriBuilder->buildUriFromRoute('record_history', [
'element' => $resultItem->getExtraData()['table'] . ':' . $resultItem->getExtraData()['uid'],
]));
$resultItem->addAction($showHistoryAction);
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
PSR-14 event to modify as result item created by the live search
getResultItem()
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\ResultItem
PageContentPreviewRenderingEvent
New in version 12.0
This event has been introduced which serves as a drop-in replacement for the removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']
hook.
Use the PSR-14 event
\TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent
to ship an alternative rendering for a specific content type or
to manipulate the record data of a content element.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Backend\EventListener;
useTYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
useTYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(
identifier: 'my-extension/preview-rendering-example-ctype',
)]
final readonly classMyEventListener{
publicfunction__invoke(PageContentPreviewRenderingEvent $event): void{
if ($event->getTable() !== 'tt_content') {
return;
}
if ($event->getRecordType() === 'example_ctype') {
$event->setPreviewContent('<div>...</div>');
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Due to the integration of EXT:recordlist into EXT:backend the namespace of
the event changed from
\TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent
to
\TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent .
For TYPO3 v12 the moved class is available as an alias under the old
namespace to allow extensions to be compatible with TYPO3 v11 and v12.
The PSR-14 event
\TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent
allows to add content before or after the main content of the List
module.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
Add content above or below the main content of the record list
getRequest()
Returns
\Psr\Http\Message\ServerRequestInterface
addContentAbove(string $contentAbove)
param $contentAbove
the contentAbove
addContentBelow(string $contentBelow)
param $contentBelow
the contentBelow
getAdditionalContentAbove()
Returns
string
getAdditionalContentBelow()
Returns
string
SudoModeRequiredEvent
New in version 12.4.32 / 13.4.13
This event was introduced by security advisory TYPO3-CORE-SA-2025-013
to address challenges with single sign-on (SSO) providers.
The PSR-14 event
\TYPO3\CMS\Backend\Backend\Event\SudoModeRequiredEvent is triggered before
showing the sudo-mode verification dialog
when managing backend user accounts.
This step-up authentication, introduced as part of the fix for
TYPO3-CORE-SA-2025-013,
helps prevent unauthorized password changes. However,
it may pose challenges when using remote single sign-on (SSO) systems, which
typically do not support a separate step-up verification process.
This event allows developers to skip / bypass the step-up
authentication process and uses custom logic, such as identifying users
authenticated through an SSO system.
See also
The SudoModeVerifyEvent
is triggered before verification of a submitted password.
Example: Use an event listener to skip step-up authentication for SSO users
The following example demonstrates how to use an event listener to skip step-up
authentication for be_users records that have an active is_sso flag:
This event was introduced by the fix for security advisory TYPO3-CORE-SA-2025-013
to address challenges with single sign-on (SSO) providers.
The PSR-14 event
\TYPO3\CMS\Backend\Backend\Event\SudoModeVerifyEvent is triggered before
a password submitted in sudo-mode verification dialog is verified
for backend user accounts.
This step-up authentication mechanism, introduced as part of the fix for
TYPO3-CORE-SA-2025-013,
may pose challenges when using remote single sign-on
(SSO) systems because they do not support a dedicated verification step.
This event allows developers to change the verification logic of step-up
authentication, by conditionally allowing or denying verification based
on custom logic — for example, by identifying users authenticated via an
SSO system.
See also
SudoModeRequiredEvent
is triggered before showing the sudo-mode verification dialog.
Example: Use an event listener to modify the verification of password in sudo mode
The following demonstrates using the event to statically check the password for
an expected hash.
Warning
This example has been simplified for clarity. Always use secure password
handling with salted hashing in production. Never hard-code a password hash as
shown below.
<?phpdeclare(strict_types=1);
namespaceExample\Demo\EventListener;
useTYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeVerifyEvent;
finalclassStaticPasswordVerification{
publicfunction__invoke(SudoModeVerifyEvent $event): void{
$calculatedHash = hash('sha256', $event->getPassword());
// static hash of `dontdothis` - just used as proof-of-concept// side-note: in production, make use of strong salted password
$expectedHash = '3382f2e21a5471b52a85bc32ab59ab2c467f6e3cb112aef295323874f423994c';
if (hash_equals($expectedHash, $calculatedHash)) {
$event->setVerified(true);
}
}
}
This event is triggered when a "SU" (switch user) action has been triggered
getSessionId()
Returns
string
getTargetUser()
Returns
array
getCurrentUser()
Returns
array
SystemInformationToolbarCollectorEvent
The PSR-14 event
\TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent
allows to enrich the system information toolbar in the TYPO3 backend top toolbar
with various information.
Example: Display release information in "System Information" toolbar window
The event
SystemInformationToolbarCollectorEvent
gets you the object
SystemInformationToolbarItem
on which you can call method addSystemInformation() to add system
information items or method addSystemMessage() to add messages.
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.
Hint
This event acts as a substitution for the removed TYPO3 hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] .
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
Event fired after user groups have been resolved for a specific user
getSourceDatabaseTable()
Return description
'be_groups' or 'fe_groups' depending on context.
Returns
string
getGroups()
List of group records including sub groups as resolved by core.
Note order is important: A user with main groups "1,2", where 1 has sub group 3,
results in "3,1,2" as record list array - sub groups are listed before the group
that includes the sub group.
Returns
array
setGroups(array $groups)
List of group records as manipulated by the event.
param $groups
the groups
getOriginalGroupIds()
List of group uids directly attached to the user
Returns
array
getUserData()
Full user record with all fields
Returns
array
AfterUserLoggedInEvent
New in version 12.3
The event replaces the deprecated hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'] .
New in version 13.0
The event is also dispatched, when a successful frontend user login is
performed.
The purpose of the PSR-14 event
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent
is to trigger any kind of action when a backend or frontend user has been
successfully logged in.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Authentication\EventListener;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Authentication\BackendUserAuthentication;
useTYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;
#[AsEventListener(
identifier: 'my-extension/after-user-logged-in',
)]
final readonly classMyEventListener{
publicfunction__invoke(AfterUserLoggedInEvent $event): void{
if (
$event->getUser() instanceof BackendUserAuthentication
&& $event->getUser()->isAdmin()
) {
// Do something like: Clear all caches after login
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The event replaces the deprecated hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] .
The purpose of the PSR-14 event
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent
is to trigger any kind of action when a user has been successfully logged out.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
The event
\TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent
allows to intercept or adjust a request token
during active user authentication process.
Example
The event can be used to generate the request token individually. This can be
the case when you are not using a login callback and have not the possibility
to submit a request token:
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The event replaces the deprecated hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] .
The purpose of the PSR-14 event
\TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent is to trigger
any kind of action before a user will be logged out.
The event has the possibility to bypass the regular logout process by TYPO3
(removing the cookie and the user session) by calling
$event->disableRegularLogoutProcess() in an event listener.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Authentication\EventListener;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent;
#[AsEventListener(
identifier: 'my-extension/login-attempt-failed',
)]
final readonly classMyEventListener{
publicfunction__invoke(LoginAttemptFailedEvent $event): void{
$normalizedParams = $event->getRequest()->getAttribute('normalizedParams');
if ($normalizedParams->getRemoteAddress() !== '198.51.100.42') {
// send an email because an external user attempt failed
}
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
This event was introduced to replace and improve the method
parseDataStructureByIdentifierPostProcess() of the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent
can be used to control the FlexForm parsing in an
object-oriented approach.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
Listeners to this event are able to modify or enhance the data structure identifier,
which is used for a given TCA flex field.
This event can be used to add additional data to an identifier. Be careful here, especially if
stuff from the source record like uid or pid is added! This may easily lead to issues with
data handler details like copy or move records, localization and version overlays.
Test this very well! Multiple listeners may add information to the same identifier here -
take care to namespace array keys. Information added here can be later used in the
data structure related PSR-14 Events (BeforeFlexFormDataStructureParsedEvent and
AfterFlexFormDataStructureParsedEvent) again.
See the note on FlexFormTools regarding the schema of $dataStructure.
getFieldTca()
Returns the full TCA of the currently handled field, having
type=flex set.
Returns
array
getTableName()
Returns
string
getFieldName()
Returns
string
getRow()
Returns the whole database row of the current record.
Returns
array
setIdentifier(array $identifier)
Allows to modify or completely replace the initialized data
structure identifier.
param $identifier
the identifier
getIdentifier()
Returns the initialized data structure identifier, which has
either been defined by an event listener or set to the default
by the FlexFormTools component.
Returns
array
AfterFlexFormDataStructureParsedEvent
New in version 12.0
This event was introduced to replace and improve the method
getDataStructureIdentifierPostProcess() of the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent
can be used to control the FlexForm parsing in an
object-oriented approach.
Listeners to this event are able to modify or enhance a flex form data
structure that corresponds to a given identifier, after it was parsed and
before it is used by further components.
Note: Since this event is not stoppable, all registered listeners are
called. Therefore, you might want to namespace your identifiers in a way,
that there is little chance they overlap (e.g. prefix with extension name).
See the note on FlexFormTools regarding the schema of $dataStructure.
getIdentifier()
Returns
array
getDataStructure()
Returns the current data structure, which has been processed and
parsed by the FlexFormTools component. Might contain additional
data from previously called listeners.
Returns
array
setDataStructure(array $dataStructure)
Allows to modify or completely replace the parsed data
structure identifier.
param $dataStructure
the dataStructure
AfterTcaCompilationEvent
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent is
dispatched after
$GLOBALS['TCA'] is built to allow to further manipulate
the TCA.
Note
It is possible to check against the original TCA as this is stored within
$GLOBALS['TCA'] before this event is fired.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
This event was introduced to replace and improve the method
getDataStructureIdentifierPreProcess() of the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent
can be used to control the FlexForm parsing in an
object-oriented approach.
Listeners to this event are able to specify the data structure identifier,
used for a given TCA flex field.
Listeners should call ->setIdentifier() to set the identifier or ignore the
event to allow other listeners to set it. Do not set an empty string as this
will immediately stop event propagation!
The identifier SHOULD include the keys specified in the Identifier definition
on FlexFormTools, and nothing else. Adding other keys may or may not work,
depending on other code that is enabled, and they are not guaranteed nor
covered by BC guarantees.
Warning: If adding source record details like the uid or pid here, this may turn out to be fragile.
Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may
break in between different core versions.
It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see
the core internal 'tca' and 'record' return values below
See the note on FlexFormTools regarding the schema of $dataStructure.
getFieldTca()
Returns the full TCA of the currently handled field, having
type=flex set.
Returns
array
getTableName()
Returns
string
getFieldName()
Returns
string
getRow()
Returns the whole database row of the current record.
Returns
array
setIdentifier(array $identifier)
Allows to define the data structure identifier for the TCA field.
Setting an identifier will immediately stop propagation. Avoid
setting this parameter to an empty array as this will also stop
propagation.
param $identifier
the identifier
getIdentifier()
Returns the current data structure identifier, which will always be
null for listeners, since the event propagation is
stopped as soon as a listener defines an identifier.
Returns
?array
isPropagationStopped()
Returns
bool
Migration
Using the removed hook method
getDataStructureIdentifierPreProcess() of
the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray']
previously required implementations to always return an
array.
This means, implementations returned an empty
array in case they did
not want to set an identifier, allowing further implementations to be
called.
This behaviour has now changed. As soon as a listener sets the identifier
using the
setIdentifier() method, the event propagation is stopped
immediately and no further listeners are being called. Therefore, listeners
should avoid setting an empty
array but should just "return" without
any change to the
$event object in such a case.
BeforeFlexFormDataStructureParsedEvent
New in version 12.0
This event was introduced to replace and improve the method
parseDataStructureByIdentifierPreProcess() of the hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent
can be used to control the FlexForm parsing in an
object-oriented approach.
Listeners to this event are able to specify a flex form data structure that
corresponds to a given identifier.
Listeners should call ->setDataStructure() to set the data structure (this
can either be a resolved data structure string, a "FILE:" reference or a
fully parsed data structure as array) or ignore the event to allow other
listeners to set it. Do not set an empty array or string as this will
immediately stop event propagation!
See the note on FlexFormTools regarding the schema of $dataStructure.
getDataStructure()
Returns the current data structure, which will always be null
for listeners, since the event propagation is stopped as soon as
a listener sets a data structure.
Returns
array|string|?null
setDataStructure(array|string $dataStructure)
Allows to either set an already parsed data structure as array,
a file reference or the XML structure as string. Setting a data
structure will immediately stop propagation. Avoid setting this parameter
to an empty array or string as this will also stop propagation.
param $dataStructure
the dataStructure
getIdentifier()
Returns
array
isPropagationStopped()
Returns
bool
Migration
Using the removed hook method
parseDataStructureByIdentifierPreProcess()
of hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray']
previously required implementations to always return an
array or
string. Implementations returned an empty
array or empty
string in case they did not want to set a data structure.
This behaviour has now changed. As soon as a listener sets a data structure
using the
setDataStructure() method, the event propagation is stopped
immediately and no further listeners are called.
Therefore, listeners should avoid setting an empty
array or an empty
string but should just "return" without any change to the
$event object in such a case.
BeforeTcaOverridesEvent
New in version 13.0
A PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent
enables developers to listen to the state between loaded base TCA and merging of
TCA overrides.
It can be used to dynamically generate TCA and add it as additional
base TCA. This is especially useful for "TCA generator" extensions, which add
TCA based on another resource, while still enabling users to override TCA via
the known TCA overrides API.
Note
$GLOBALS['TCA'] is not set at this point. Event listeners can only
work on the TCA coming from
$event->getTca() and
must not access
$GLOBALS['TCA'] .
TCA is always "runtime-cached". This means that dynamic
additions must never depend on runtime state, for example, the current
PSR-7 request or similar, because such information
might not even exist when the first call is done, for example, from
CLI.
Event before $tca which later becomes $GLOBALS['TCA'] is overridden by TCA/Overrides.
Allows to manipulate $tca, before overrides are merged.
getTca()
Returns
array
setTca(array $tca)
param $tca
the tca
ModifyLoadedPageTsConfigEvent
Extensions can modify page TSconfig
entries that can be overridden or added, based on the root line.
Changed in version 12.2
The event has moved its namespace from
\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent to
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent .
Apart from that no changes were made. TYPO3 v12 triggers both the old
and the new event, and TYPO3 v13 stopped calling the old event.
Extensions can modify page TSconfig entries that can be overridden or added, based on the root line
getTsConfig()
Returns
array
addTsConfig(string $tsConfig)
param $tsConfig
the tsConfig
setTsConfig(array $tsConfig)
param $tsConfig
the tsConfig
getRootLine()
Returns
array
SiteConfigurationBeforeWriteEvent
New in version 12.0
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent
allows the modification of the site configuration array
before writing the configuration to disk.
Event fired before a site configuration is written to a yaml file
allows dynamic modification of the site's configuration before writing.
getSiteIdentifier()
Returns
string
getConfiguration()
Returns
array
setConfiguration(array $configuration)
param $configuration
overwrite the configuration array of the site
SiteConfigurationLoadedEvent
New in version 12.0
The PSR-14 event
\TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent
allows the modification of the site configuration array
before loading the configuration.
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
The PSR-14 event
\TYPO3\CMS\Core\Core\Event\BootCompletedEvent is fired
on every request when TYPO3 has been fully booted, right after all configuration
files have been added.
This event complements the AfterTcaCompilationEvent which is executed
after TCA configuration has been assembled.
Use cases for this event include running extension's code which needs to be
executed at any time and needs TYPO3's full configuration including all loaded
extensions.
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\Bootstrap\EventListener;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Core\Event\BootCompletedEvent;
#[AsEventListener(
identifier: 'my-extension/boot-completed',
)]
final readonly classMyEventListener{
publicfunction__invoke(BootCompletedEvent $e): void{
// Do your magic
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
API
classBootCompletedEvent
Fully qualified name
\TYPO3\CMS\Core\Core\Event\BootCompletedEvent
Executed when TYPO3 has fully booted (after all ext_tables.php files have been processed)
isCachingEnabled()
Returns
bool
Country
The following list contains PSR-14 events
in EXT:core, namespace Country.
The PSR-14 event
\TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent
allows to modify the list of countries provided by
the
\TYPO3\CMS\Core\Country\CountryProvider .
<?phpdeclare(strict_types=1);
namespaceMyVendor\MyExtension\EventListener;
useTYPO3\CMS\Core\Attribute\AsEventListener;
useTYPO3\CMS\Core\Country\Country;
useTYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent;
final readonly classEventListener{
#[AsEventListener(identifier: 'my-extension/before-countries-evaluated')]publicfunction__invoke(BeforeCountriesEvaluatedEvent $event): void{
$countries = $event->getCountries();
unset($countries['BS']);
$countries['XX'] = new Country(
'XX',
'XYZ',
'Magic Kingdom',
'987',
'🔮',
'Kingdom of Magic and Wonders',
);
$event->setCountries($countries);
}
}
Copied!
New in version 13.0
The PHP attribute
\TYPO3\CMS\Core\Attribute\AsEventListener has been
introduced to tag a PHP class as an event listener. Alternatively, or if you
need to be compatible with older TYPO3 versions, you can also register an
event listener via the Configuration/Services.yaml file. Switch to
an older version of this page for an example or have a look at the section
Implementing an event listener in your extension.
As the localized names for the countries are defined in file
EXT:core/Resources/Private/Language/Iso/countries.xlf, this language
file needs to be extended via
locallangXMLOverride:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?><xliffversion="1.0"><filesource-language="en"datatype="plaintext"date="2024-01-08T18:44:59Z"product-name="my_extension"><body><trans-unitid="XX.name"approved="yes"><source>Magic Kingdom</source></trans-unit><trans-unitid="XX.official_name"approved="yes"><source>Kingdom of Magic and Wonders</source></trans-unit></body></file></xliff>
<?xml version="1.0" encoding="utf-8" standalone="yes" ?><xliffversion="1.0"><filesource-language="en"target-language="de"datatype="plaintext"date="2024-01-08T18:44:59Z"product-name="my_extension"><body><trans-unitid="XX.name"approved="yes"><source>Magic Kingdom</source><target>Magisches Königreich</target></trans-unit><trans-unitid="XX.official_name"approved="yes"><source>Kingdom of Magic and Wonders</source><target>Königreich der Magie und Wunder</target></trans-unit></body></file></xliff>
<?xml version="1.0" encoding="utf-8" standalone="yes" ?><xliffversion="1.0"><filesource-language="en"target-language="tlh"datatype="plaintext"date="2024-01-08T18:44:59Z"product-name="my_extension"><body><trans-unitid="XX.name"approved="yes"><source>Magic Kingdom</source><target>‘oHtaHghach wo’</target></trans-unit><trans-unitid="XX.official_name"approved="yes"><source>Kingdom of Magic and Wonders</source><target>‘oHtaHghach je Dojmey wo’</target></trans-unit></body></file></xliff>
The PSR-14 event
\TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent
allows to intercept the
CREATE TABLE statement from all loaded
extensions.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
The PSR-14 event
\TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent
is fired so listeners can intercept and add elements when checking
links within the soft reference parser.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
Event fired so listeners can intercept add elements when checking links within the SoftRef parser
getLinkParts()
Returns
array
getContent()
Returns
string
getElements()
Returns
array
getIdx()
Returns
int
getTokenId()
Returns
string
setLinkParts(array $linkParts)
param $linkParts
the linkParts
setContent(string $content)
param $content
the content
setElements(array $elements)
param $elements
the elements
addElements(array $elements)
param $elements
the elements
isResolved()
Returns
bool
IsTableExcludedFromReferenceIndexEvent
The PSR-14 event
\TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent
allows to intercept, if a certain table should be excluded from the
Reference Index.
There is no need to add tables without a definition in
$GLOBALS['TCA']
since the reference index only handles those.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
This event serves as a replacement for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] .
The PSR-14 event
\TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent
can be used to modify the actual translated record (if found) to add additional
information or perform custom processing of the record.
Event which is fired after a record was translated (or tried to be localized).
getTable()
Returns
string
getRecord()
Returns
array
getLanguageAspect()
Returns
\TYPO3\CMS\Core\Context\LanguageAspect
setLocalizedRecord(?array $localizedRecord)
param $localizedRecord
the localizedRecord
getLocalizedRecord()
Returns
?array
overlayingWasAttempted()
Determines if the overlay functionality happened, thus, returning the lo
Returns
bool
BeforePageIsRetrievedEvent
New in version 13.0
This event serves as a more powerful replacement for the removed
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage']
hook.
The PSR-14 event
\TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent
allows to modify the resolving of page records within
\TYPO3\CMS\Core\Domain\PageRepository->getPage().
It can be used to alter the incoming page ID or to even fetch a fully-loaded
page object before the default TYPO3 behaviour is executed, effectively
bypassing the default page resolving.
Event which is fired before a page (id) is being resolved from PageRepository.
Allows to change the corresponding page ID, e.g. to resolve a different page
with custom overlaying, or to fully resolve the page on your own.
getPage()
Returns
?\TYPO3\CMS\Core\Domain\Page
setPage(\TYPO3\CMS\Core\Domain\Page $page)
param $page
the page
hasPage()
Returns
bool
getPageId()
Returns
int
setPageId(int $pageId)
param $pageId
the pageId
skipGroupAccessCheck()
respectGroupAccessCheck()
isGroupAccessCheckSkipped()
Returns
bool
getContext()
Returns
\TYPO3\CMS\Core\Context\Context
BeforePageLanguageOverlayEvent
New in version 12.0
This event serves as a replacement for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPageOverlay'] .
The PSR-14 event
\TYPO3\CMS\Core\Domain\Event\BeforePageLanguageOverlayEvent
is a special event which is fired when TYPO3 is about to do the language overlay
of one or multiple pages, which could be one full record or multiple page IDs.
This event is fired only for pages and in-between the events
BeforeRecordLanguageOverlayEvent and AfterRecordLanguageOverlayEvent.
Example
Note
Currently, we do not have an example for this event. If you can provide a
useful one, please open an issue with your code snippets or
a pull request.
This event serves as replacement for the removed hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] .
The PSR-14 event
\TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent
can be used to modify information (such as the
LanguageAspect or the actual incoming
record from the database) before the database is queried.
Example: Change the overlay type to "on" (connected)
In this example, we will change the overlay type to "on" (connected). This may
be necessary if your site is configured with free mode, but you have a record
type that has languages connected.