Important: #101699 - Messenger wildcard routes act as a fallback
See forge#101699
Description
The Symfony Messenger transport routing configured via
$GLOBALS maps a message
type to one or more transport identifiers.
So far a wildcard route - a namespace wildcard (
My\) or the
global wildcard
'*'
- was always applied in addition to any matching
specific route. A message that matched both a specific route and a wildcard
route was therefore dispatched to both transports. Because TYPO3 ships a default
catch-all route
'*' => 'default'
, every routed message was silently sent
to the default transport as well, and routing a message to its specific
transport only required removing the wildcard route explicitly, typically in
an extension's ext_:
// previously required to avoid the additional wildcard routing
unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*']);
A wildcard route now acts as a fallback: it is skipped as soon as a more
specific type already matched, and only applies when no specific route matched.
This mirrors the upstream Symfony
Senders resolution, so the
shipped default
'*' => 'default'
route now behaves as a true fallback and
the manual
unset of the wildcard route is no longer needed:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
// handled by the "doctrine" transport only, the '*' route is skipped
\My\Extension\Message\ImportMessage::class => 'doctrine',
// fallback for every message without a more specific route
'*' => 'default',
];
Single and multiple transports per route
A routing value may be given as a single transport identifier (a string) or as an ordered list of transport identifiers (an array) to route a single message to several transports on purpose:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
// single transport
\My\Extension\Message\ImportMessage::class => 'doctrine',
// sent to "audit" and "doctrine", in that order
\My\Extension\Message\AuditableInterface::class => ['audit', 'doctrine'],
'*' => 'default',
];
The legacy mapping syntax using a plain string
(
\My\) keeps working unchanged and is treated
as the single-element array form (
\My\).
Wildcards are supported on every namespace level
Wildcard routing is not limited to the global
'*'
route. A namespace
wildcard may be configured on any level and is likewise treated as a fallback
that is only used when no more specific route matched:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
// every message below \My\Extension\Message\Import\
\My\Extension\Message\Import\* => 'import',
// every other message below \My\Extension\Message\
\My\Extension\Message\* => 'doctrine',
// global fallback
'*' => 'default',
];
How a message is matched against the routing configuration
The routing configuration is a plain lookup map; its key order is irrelevant.
The order in which routes are considered comes from the type chain of the
dispatched message, derived from the message's PHP type (via
\Symfony\), not
from the order of the entries in the global configuration array.
For a dispatched message the type chain is evaluated from most to least specific:
- the concrete message class,
- its parent classes (bottom-up),
- its implemented interfaces,
- the namespace wildcards for every namespace level (for example
My\,Extension\ Message\ Import\* My\,Extension\ Message\* My\,Extension\* My\*), - and finally the global wildcard
'*'.
Senders are collected across every matching type in that order and deduplicated
(first occurrence wins). The first non-wildcard match therefore "locks in" the
transports and every following wildcard route is skipped. This resolution
depends solely on the class, parent and interface hierarchy of the message and
is entirely independent of the shape or ordering of the
['SYS'] array - the configuration only provides
the mapping from a type identifier to its transports.
Impact
Installations that (knowingly or not) relied on a message being routed to both
its specific transport and the wildcard transport - most notably the default
'*' => 'default'
route - will see such a message routed to its specific
transport only. Configure the additional transport explicitly as a list value
(
\My\) where the previous
fan-out is still wanted.
Providing this change on older TYPO3 versions
The change is contained in the core
\TYPO3\. Projects that need the
fallback resolution on an older TYPO3 version that does not ship it yet (for
example TYPO3 v12) can obtain it in one of two ways.
Replace the core implementation via Symfony dependency injection
Copy the current
Transport into an extension, for example as
\My, keeping the
#
constructor argument. Then re-point the messenger senders-locator to it in the
extension's Configuration/:
services:
MyVendor\MyExtension\Messenger\FallbackTransportLocator:
arguments:
$sendersLocator: !tagged_locator { tag: 'messenger.sender', index_by: 'identifier' }
# replace the core implementation used for message routing
TYPO3\CMS\Core\Messenger\TransportLocator:
alias: MyVendor\MyExtension\Messenger\FallbackTransportLocator
Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface:
alias: MyVendor\MyExtension\Messenger\FallbackTransportLocator
Because the messenger senders-locator is resolved through this alias, the
extension implementation is used instead of the core one and the fallback
resolution becomes available on the older core version. If the
Webhook display of the resolved transports is required as
well, copy its adjusted version accordingly.
Backport the change via a Composer patch
Alternatively, apply the change to typo3/ directly using a
Composer patch tool such as cweagans/composer-patches:
{
"require": {
"cweagans/composer-patches": "^1.7"
},
"extra": {
"patches": {
"typo3/cms-core": {
"Messenger wildcard routing fallback": "patches/messenger-wildcard-fallback.patch"
}
}
}
}
The patch file should contain the diff of
typo3/ (and, if used,
the Webhook adjustment as a
typo3/cms-webhooks
patch file) and is applied automatically on
composer install
.
Note
Due to the monorepo to composer package splitting this requires adoption of
the paths and stripping out changes in Tests/ folders, and for each
affected TYPO3 system extension a dedicated patch file needs to be created
and provided.
See Applying Core patches for further details on how to create and apply Composer patches for TYPO3.