Message bus
TYPO3 provides a message bus solution based on symfony/messenger. It has the ability to send messages and then handle them immediately (synchronous) or send them through transports (asynchronous, for example, queues) to be handled later.
For backwards compatibility, the default implementation uses the synchronous transport. This means that the message bus will behave exactly as before, but it will be possible to switch to a different (asynchronous) transport on a per-project base.
To offer asynchronicity, TYPO3 also provides a transport implementation based on the Doctrine DBAL messenger transport from Symfony and a basic implementation of a consumer command.
See also
To familiarize yourself with the concept, please also read the following resources:
More details and an example implementation are described in this blog post:
Table of Contents
"Everyday" usage - as a developer
Dispatch a message
-
Add a PHP class for your message object (which is an arbitrary PHP class)
EXT:my_extension/Classes/Queue/Message/DemoMessage.php -
Inject the
Messageinto your class and call theBus Interface dispatchmethod() EXT:my_extension/Classes/MyClass.php<?php declare(strict_types=1); namespace MyVendor\MyExtension; use MyVendor\MyExtension\Queue\Message\DemoMessage; use Symfony\Component\Messenger\MessageBusInterface; final class MyClass { public function __construct( private readonly MessageBusInterface $bus, ) {} public function doSomething(): void { // ... $this->bus->dispatch(new DemoMessage('test')); // ... } }
Register a handler
Implement the handler class
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Queue\Handler;
use MyVendor\MyExtension\Queue\Message\DemoMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class DemoHandler
{
public function __invoke(DemoMessage $message): void
{
// do something with $message
}
}
The message handler can be registered using the symfony PHP attribute
\Symfony\.
A Services. entry is needed to use
before
/
after
for registration if you need to define an order:
MyVendor\MyExtension\Queue\Handler\DemoHandler:
tags:
- name: 'messenger.message_handler'
# Define another handler which should be called before DemoHandler:
MyVendor\MyExtension\Queue\Handler\DemoHandler2:
tags:
- name: 'messenger.message_handler'
before: 'MyVendor\MyExtension\Queue\Handler\DemoHandler'
"Everyday" usage - as a system administrator/integrator
By default, TYPO3 will behave like in versions before TYPO3 v12. This means that the message bus will use the synchronous transport and all messages will be handled immediately. To benefit from the message bus, it is recommended to switch to an asynchronous transport. Using asynchronous transports increases the resilience of the system by decoupling external dependencies even further.
Currently, the TYPO3 Core provides an asynchronous transport based on the Doctrine DBAL messenger transport. This transport is configured to use the default TYPO3 database connection. It is pre-configured and can be used by changing the settings:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*'] = 'doctrine';
This will route all messages to the asynchronous transport (mind the
*
).
Attention
If you are using the Doctrine transport, make sure to take care of running the consume command.
Async message handling - the consume command
To consume messages, run the command:
vendor/bin/typo3 messenger:consume <receiver-name>
typo3/sysext/core/bin/typo3 messenger:consume <receiver-name>
By default, you should run:
vendor/bin/typo3 messenger:consume doctrine
typo3/sysext/core/bin/typo3 messenger:consume doctrine
The command takes almost all options of the Symfony command of the same name.
Changed in version 14.2 Feature: #106261 - Align command line arguments of message consumer with Symfony original
The worker options below were added. Before, the command stopped after one
hour on its own and reported that through an --
option. Both are gone: a worker now runs until you give it a limit.
A worker does not stop on its own. In continuous operation, configure a limit so that the PHP process is periodically recycled to release memory:
--,time- limit -t- Stop after this number of seconds.
--,limit -l- Stop after this number of messages.
--,failure- limit -f- Stop after this number of failed messages.
--,memory- limit -m- Stop when the worker uses more than this much memory, for example
128M.
A limit only takes effect after the current message is handled, so the worker never stops in the middle of one. These options select what the worker reads and how it waits:
--all - Consume from every configured receiver.
--,bus -b- Dispatch the received messages to this bus. Without it, TYPO3 picks the bus itself.
--queues - Consume only from these queues of the receiver. Repeat the option per queue.
--sleep - Wait this number of seconds before asking for messages again when none
were found. The default is
1. --keepalive - Keep the connection of the transport alive, if the transport implements it. The default interval is 5 seconds.
Whichever limit is reached, the worker process exits with code 0. Run it
under a service manager like systemd that keeps the service active and
automatically restarts a fresh worker process.
The following code provides an example for a service. Create the following file on your server:
[Unit]
Description=Run the TYPO3 message consumer
Requires=mariadb.service
After=mariadb.service
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/php8.5 /var/www/myproject/vendor/bin/typo3 messenger:consume doctrine --time-limit=3600
# The command exits with 0 on its time limit, so restart it either way
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
Where no service manager is available, the command can run as a scheduler task instead.
Important
The messenger: command blocks subsequent scheduler tasks from
executing while it is running. Set -- to a value lower than the
scheduler's cron interval (for example, 240 seconds for a 5-minute cron
interval) so that the task completes before the next run.
Advanced usage
Configure a custom transport (senders/receivers)
Transports are configured in the services configuration. To allow the
configuration of a transport per message, the TYPO3 configuration
(settings.php, additional.php on system level, or
ext_localconf.php in an extension) is utilized. The transport/sender
name used in the settings is resolved to a service that has been tagged with
message. and the respective identifier.
<?php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger'] = [
'routing' => [
// Use "messenger.transport.demo" as transport for DemoMessage
\MyVendor\MyExtension\Queue\Message\DemoMessage::class => 'demo',
// Use "messenger.transport.default" as transport for all other messages
'*' => 'default',
],
];
messenger.transport.demo:
factory: [ '@TYPO3\CMS\Core\Messenger\DoctrineTransportFactory', 'createTransport' ]
class: 'Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport'
arguments:
$options:
queue_name: 'demo'
tags:
- name: 'messenger.sender'
identifier: 'demo'
- name: 'messenger.receiver'
identifier: 'demo'
messenger.transport.default:
factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
arguments:
$dsn: 'in-memory://default'
$options: [ ]
tags:
- name: 'messenger.sender'
identifier: 'default'
- name: 'messenger.receiver'
identifier: 'default'
The TYPO3 Core has been tested with three transports:
\Symfony\(default)Component\ Messenger\ Transport\ Sync\ Sync Transport \Symfony\(using the Doctrine DBAL messenger transport)Component\ Messenger\ Bridge\ Doctrine\ Transport\ Doctrine Transport \Symfony\(for testing)Component\ Messenger\ Transport\ In Memory\ In Memory Transport
Add rate limiter
Rate limiting can be applied to asynchronous messages processed through the consume command. This allows controlling message processing rates to:
- Stay within external service limits (API quotas, mail sending thresholds)
- Manage server resource utilization
Example: usage of a rate limiter
Use the following configuration to limit the process of messages to max. 100 each 60 seconds:
messenger.rate_limiter.limit_db_messages:
class: Symfony\Component\RateLimiter\RateLimiterFactory
arguments:
$config:
id: 'limit_db_messages'
policy: 'sliding_window'
limit: 100
interval: '60 seconds'
$storage: '@TYPO3\CMS\Core\RateLimiter\Storage\CachingFrameworkStorage'
tags:
- name: 'messenger.rate_limiter'
identifier: 'doctrine'
Hint
As TYPO3 default transport for asynchronous messages is doctrine you also
have to set the tags identifier to doctrine.
InMemoryTransport for testing
The
In is a transport that should only be used while
testing.
messenger.transport.default:
factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
public: true
arguments:
$dsn: 'in-memory://default'
$options: [ ]
tags:
- name: 'messenger.sender'
identifier: 'default'
- name: 'messenger.receiver'
identifier: 'default'
Configure a custom middleware
The middleware is set up in the services configuration. By default, the
\Symfony\ and the
\Symfony\ are
registered. See also the Custom middleware section in the Symfony
documentation.
To add your own middleware, tag it as
messenger. and set the
order using TYPO3's
before
and
after
ordering mechanism:
Symfony\Component\Messenger\Middleware\SendMessageMiddleware:
arguments:
$sendersLocator: '@Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface'
$eventDispatcher: '@Psr\EventDispatcher\EventDispatcherInterface'
tags:
- { name: 'messenger.middleware' }
Symfony\Component\Messenger\Middleware\HandleMessageMiddleware:
arguments:
$handlersLocator: '@Symfony\Component\Messenger\Handler\HandlersLocatorInterface'
tags:
- name: 'messenger.middleware'
after: 'Symfony\Component\Messenger\Middleware\SendMessageMiddleware'