* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Messenger\DependencyInjection; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocator; use Symfony\Component\Messenger\TraceableMessageBus; use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; /** * @author Samuel Roze */ class MessengerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $busIds = []; foreach ($container->findTaggedServiceIds('messenger.bus') as $busId => $tags) { $busIds[] = $busId; if ($container->hasParameter($busMiddlewareParameter = $busId.'.middleware')) { $this->registerBusMiddleware($container, $busId, $container->getParameter($busMiddlewareParameter)); $container->getParameterBag()->remove($busMiddlewareParameter); } if ($container->hasDefinition('data_collector.messenger')) { $this->registerBusToCollector($container, $busId); } } if ($container->hasDefinition('messenger.receiver_locator')) { $this->registerReceivers($container, $busIds); } $this->registerHandlers($container, $busIds); } private function registerHandlers(ContainerBuilder $container, array $busIds): void { $definitions = []; $handlersByBusAndMessage = []; $handlerToOriginalServiceIdMapping = []; foreach ($container->findTaggedServiceIds('messenger.message_handler', true) as $serviceId => $tags) { foreach ($tags as $tag) { if (isset($tag['bus']) && !\in_array($tag['bus'], $busIds, true)) { throw new RuntimeException(sprintf('Invalid handler service "%s": bus "%s" specified on the tag "messenger.message_handler" does not exist (known ones are: "%s").', $serviceId, $tag['bus'], implode('", "', $busIds))); } $className = $this->getServiceClass($container, $serviceId); $r = $container->getReflectionClass($className); if (null === $r) { throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $serviceId, $className)); } if (isset($tag['handles'])) { $handles = isset($tag['method']) ? [$tag['handles'] => $tag['method']] : [$tag['handles']]; } else { $handles = $this->guessHandledClasses($r, $serviceId, $tag['method'] ?? '__invoke'); } $message = null; $handlerBuses = (array) ($tag['bus'] ?? $busIds); foreach ($handles as $message => $options) { $buses = $handlerBuses; if (\is_int($message)) { if (\is_string($options)) { $message = $options; $options = []; } else { throw new RuntimeException(sprintf('The handler configuration needs to return an array of messages or an associated array of message and configuration. Found value of type "%s" at position "%d" for service "%s".', get_debug_type($options), $message, $serviceId)); } } if (\is_string($options)) { $options = ['method' => $options]; } $options += array_filter($tag); unset($options['handles']); $priority = $options['priority'] ?? 0; $method = $options['method'] ?? '__invoke'; $fromTransport = $options['from_transport'] ?? ''; if (isset($options['bus'])) { if (!\in_array($options['bus'], $busIds)) { $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method); throw new RuntimeException(sprintf('Invalid configuration '.$messageLocation.' for message "%s": bus "%s" does not exist.', $message, $options['bus'])); } $buses = [$options['bus']]; } if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method); throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" '.$messageLocation.' not found.', $serviceId, $message)); } if (!$r->hasMethod($method)) { throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method)); } if ('__invoke' !== $method || '' !== $fromTransport) { $wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method.':'.$fromTransport)] = $wrapperDefinition; } else { $definitionId = $serviceId; } $handlerToOriginalServiceIdMapping[$definitionId] = $serviceId; foreach ($buses as $handlerBus) { $handlersByBusAndMessage[$handlerBus][$message][$priority][] = [$definitionId, $options]; } } if (null === $message) { throw new RuntimeException(sprintf('Invalid handler service "%s": the list of messages to handle is empty.', $serviceId)); } } } foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { foreach ($handlersByMessage as $message => $handlersByPriority) { krsort($handlersByPriority); $handlersByBusAndMessage[$bus][$message] = array_merge(...$handlersByPriority); } } $handlersLocatorMappingByBus = []; foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { foreach ($handlersByMessage as $message => $handlers) { $handlerDescriptors = []; foreach ($handlers as $handler) { $definitions[$definitionId = '.messenger.handler_descriptor.'.ContainerBuilder::hash($bus.':'.$message.':'.$handler[0])] = (new Definition(HandlerDescriptor::class))->setArguments([new Reference($handler[0]), $handler[1]]); $handlerDescriptors[] = new Reference($definitionId); } $handlersLocatorMappingByBus[$bus][$message] = new IteratorArgument($handlerDescriptors); } } $container->addDefinitions($definitions); foreach ($busIds as $bus) { $container->register($locatorId = $bus.'.messenger.handlers_locator', HandlersLocator::class) ->setArgument(0, $handlersLocatorMappingByBus[$bus] ?? []) ; if ($container->has($handleMessageId = $bus.'.middleware.handle_message')) { $container->getDefinition($handleMessageId) ->replaceArgument(0, new Reference($locatorId)) ; } } if ($container->hasDefinition('console.command.messenger_debug')) { $debugCommandMapping = $handlersByBusAndMessage; foreach ($busIds as $bus) { if (!isset($debugCommandMapping[$bus])) { $debugCommandMapping[$bus] = []; } foreach ($debugCommandMapping[$bus] as $message => $handlers) { foreach ($handlers as $key => $handler) { $debugCommandMapping[$bus][$message][$key][0] = $handlerToOriginalServiceIdMapping[$handler[0]]; } } } $container->getDefinition('console.command.messenger_debug')->replaceArgument(0, $debugCommandMapping); } } private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId, string $methodName): iterable { try { $method = $handlerClass->getMethod($methodName); } catch (\ReflectionException) { throw new RuntimeException(sprintf('Invalid handler service "%s": class "%s" must have an "%s()" method.', $serviceId, $handlerClass->getName(), $methodName)); } if (0 === $method->getNumberOfRequiredParameters()) { throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" requires at least one argument, first one being the message it handles.', $serviceId, $handlerClass->getName(), $methodName)); } $parameters = $method->getParameters(); /** @var \ReflectionNamedType|\ReflectionUnionType|null */ $type = $parameters[0]->getType(); if (!$type) { throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::%s()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $methodName)); } if ($type instanceof \ReflectionUnionType) { $types = []; $invalidTypes = []; foreach ($type->getTypes() as $type) { if (!$type->isBuiltin()) { $types[] = (string) $type; } else { $invalidTypes[] = (string) $type; } } if ($types) { return ('__invoke' === $methodName) ? $types : array_fill_keys($types, $methodName); } throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), implode('|', $invalidTypes))); } if ($type->isBuiltin()) { throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::%s()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $methodName, $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type)); } return ('__invoke' === $methodName) ? [$type->getName()] : [$type->getName() => $methodName]; } private function registerReceivers(ContainerBuilder $container, array $busIds): void { $receiverMapping = []; $failureTransportsMap = []; if ($container->hasDefinition('console.command.messenger_failed_messages_retry')) { $commandDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry'); $globalReceiverName = $commandDefinition->getArgument(0); if (null !== $globalReceiverName) { if ($container->hasAlias('messenger.failure_transports.default')) { $failureTransportsMap[$globalReceiverName] = new Reference('messenger.failure_transports.default'); } else { $failureTransportsMap[$globalReceiverName] = new Reference('messenger.transport.'.$globalReceiverName); } } } foreach ($container->findTaggedServiceIds('messenger.receiver') as $id => $tags) { $receiverClass = $this->getServiceClass($container, $id); if (!is_subclass_of($receiverClass, ReceiverInterface::class)) { throw new RuntimeException(sprintf('Invalid receiver "%s": class "%s" must implement interface "%s".', $id, $receiverClass, ReceiverInterface::class)); } $receiverMapping[$id] = new Reference($id); foreach ($tags as $tag) { if (isset($tag['alias'])) { $receiverMapping[$tag['alias']] = $receiverMapping[$id]; if ($tag['is_failure_transport'] ?? false) { $failureTransportsMap[$tag['alias']] = $receiverMapping[$id]; } } } } $receiverNames = []; foreach ($receiverMapping as $name => $reference) { $receiverNames[(string) $reference] = $name; } $buses = []; foreach ($busIds as $busId) { $buses[$busId] = new Reference($busId); } if ($hasRoutableMessageBus = $container->hasDefinition('messenger.routable_message_bus')) { $container->getDefinition('messenger.routable_message_bus') ->replaceArgument(0, ServiceLocatorTagPass::register($container, $buses)); } if ($container->hasDefinition('console.command.messenger_consume_messages')) { $consumeCommandDefinition = $container->getDefinition('console.command.messenger_consume_messages'); if ($hasRoutableMessageBus) { $consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus')); } $consumeCommandDefinition->replaceArgument(4, array_values($receiverNames)); try { $consumeCommandDefinition->replaceArgument(6, $busIds); } catch (OutOfBoundsException) { // ignore to preserve compatibility with symfony/framework-bundle < 5.4 } } if ($container->hasDefinition('console.command.messenger_setup_transports')) { $container->getDefinition('console.command.messenger_setup_transports') ->replaceArgument(1, array_values($receiverNames)); } if ($container->hasDefinition('console.command.messenger_stats')) { $container->getDefinition('console.command.messenger_stats') ->replaceArgument(1, array_values($receiverNames)); } $container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping); $failureTransportsLocator = ServiceLocatorTagPass::register($container, $failureTransportsMap); $failedCommandIds = [ 'console.command.messenger_failed_messages_retry', 'console.command.messenger_failed_messages_show', 'console.command.messenger_failed_messages_remove', ]; foreach ($failedCommandIds as $failedCommandId) { if ($container->hasDefinition($failedCommandId)) { $definition = $container->getDefinition($failedCommandId); $definition->replaceArgument(1, $failureTransportsLocator); } } } private function registerBusToCollector(ContainerBuilder $container, string $busId): void { $container->setDefinition( $tracedBusId = 'debug.traced.'.$busId, (new Definition(TraceableMessageBus::class, [new Reference($tracedBusId.'.inner')]))->setDecoratedService($busId) ); $container->getDefinition('data_collector.messenger')->addMethodCall('registerBus', [$busId, new Reference($tracedBusId)]); } private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection): void { $middlewareReferences = []; foreach ($middlewareCollection as $middlewareItem) { $id = $middlewareItem['id']; $arguments = $middlewareItem['arguments'] ?? []; if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) { $messengerMiddlewareId = $id; } if (!$container->has($messengerMiddlewareId)) { throw new RuntimeException(sprintf('Invalid middleware: service "%s" not found.', $id)); } if ($container->findDefinition($messengerMiddlewareId)->isAbstract()) { $childDefinition = new ChildDefinition($messengerMiddlewareId); $childDefinition->setArguments($arguments); if (isset($middlewareReferences[$messengerMiddlewareId = $busId.'.middleware.'.$id])) { $messengerMiddlewareId .= '.'.ContainerBuilder::hash($arguments); } $container->setDefinition($messengerMiddlewareId, $childDefinition); } elseif ($arguments) { throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id)); } $middlewareReferences[$messengerMiddlewareId] = new Reference($messengerMiddlewareId); } $container->getDefinition($busId)->replaceArgument(0, new IteratorArgument(array_values($middlewareReferences))); } private function getServiceClass(ContainerBuilder $container, string $serviceId): string { while (true) { $definition = $container->findDefinition($serviceId); if (!$definition->getClass() && $definition instanceof ChildDefinition) { $serviceId = $definition->getParent(); continue; } return $definition->getClass(); } } }