Feature: #97700 - Adopt Symfony Messenger as a message bus and queue
See forge#97700
Description
This feature provides a basic implementation of a message bus based on the Symfony Messenger component. 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 (async) transport on a per-project base. To offer asynchronicity, the feature also provides a transport implementation based on the Doctrine DBAL messenger transport from Symfony and a basic implementation of a consumer command.
As an example, the workspace Stage
has been rebuilt as a
message and corresponding handler.
"Everyday" usage - as a developer
Dispatch a message
-
Add a PHP class for your message object (arbitrary PHP class) (
Demo
)Message <?php namespace TYPO3\CMS\Queue\Message; final class DemoMessage { public function __construct(public readonly string $content) { } }
Copied! - Inject
\Symfony\
into your classComponent\ Messenger\ Message Bus Interface -
Call
dispatch
method with a message as argument() public function __construct(private readonly MessageBusInterface $bus) { } public function yourMethod(): void { // ... $this->bus->dispatch(new DemoMessage('test')); // ... }
Copied!
Register a handler
Use a tag to register a handler. Use before/after to define order.
Define handled message by argument type reflection or by key message
.
namespace TYPO3\CMS\Queue\Handler;
use TYPO3\CMS\Queue\Message\DemoMessage;
class DemoHandler
{
public function __invoke(DemoMessage $message): void
{
// do something with $message
}
}
TYPO3\CMS\Queue\Handler\DemoHandler:
tags:
- name: 'messenger.message_handler'
TYPO3\CMS\Queue\Handler\DemoHandler2:
tags:
- name: 'messenger.message_handler'
before: 'TYPO3\CMS\Queue\Handler\DemoHandler'
Everyday Usage - as a sysadmin/integrator
By default, the system behaves as before. This means that the message bus uses the synchronous transport and all messages are 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.
The TYPO3 Core currently 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 in config/
:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*'] = 'doctrine';
This will route all messages to the asynchronous transport.
If you are using the Doctrine transport, make sure to take care of running the consume command (see below).
Async message handling - The consume command
Run the command ./
to consume messages.
By default, you should run ./
. The command is a
slimmed-down wrapper for the Symfony command messenger:
, it only provides
the basic consumption functionality. As this command is running as a worker,
it is stopped after 1 hour to avoid memory leaks. The command should therefore
be run from a service manager like systemd
to automatically restart it after
the command exits due to the time limit.
Create a service via /etc/
:
[Unit]
Description=Run the TYPO3 message consumer
Requires=mariadb.service
After=mariadb.service
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/php8.1 /var/www/myproject/vendor/bin/typo3 messenger:consume doctrine --exit-code-on-limit 133
# Generally restart on error
Restart=on-failure
# Restart on exit code 133 (which is returned by the command when limits are reached)
RestartForceExitStatus=133
# ..but do not interpret exit code 133 as an error (as it's just a restart request)
SuccessExitStatus=133
[Install]
WantedBy=multi-user.target
The message worker can than be enabled and started via
systemctl enable --
Advanced Usage
Configure a custom transport (senders/receivers)
Set up transports in services configuration. To configure one transport per
message, the TYPO3 configuration (config/
,
config/
on system level or ext_
) is
used. The transport/sender name used in the settings is
resolved to a service that has been tagged with message.
and the
respective identifier.
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger'] = [
'routing' => [
// use "messenger.transport.demo" as transport for DemoMessage
\TYPO3\CMS\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
InMemoryTransport for testing
\Symfony\
is a
transport that should only be used while testing. See the SymfonyCasts
tutorial
for more details.
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
Set up a middleware in the services configuration. By default,
\Symfony\
and \Symfony\
are registered - see also Symfony's documentation.
To add your own message 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'