3 namespace Drupal\Core\DependencyInjection\Compiler;
5 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
6 use Symfony\Component\DependencyInjection\ContainerBuilder;
7 use Symfony\Component\DependencyInjection\Exception\LogicException;
8 use Symfony\Component\DependencyInjection\Reference;
11 * Collects services to add/inject them into a consumer service.
13 * This mechanism allows a service to get multiple processor services injected,
14 * in order to establish an extensible architecture.
16 * It differs from the factory pattern in that processors are not lazily
17 * instantiated on demand; the consuming service receives instances of all
18 * registered processors when it is instantiated. Unlike a factory service, the
19 * consuming service is not ContainerAware.
21 * It differs from plugins in that all processors are explicitly registered by
22 * service providers (driven by declarative configuration in code); the mere
23 * availability of a processor (cf. plugin discovery) does not imply that a
24 * processor ought to be registered and used.
26 * It differs from regular service definition arguments (constructor injection)
27 * in that a consuming service MAY allow further processors to be added
28 * dynamically at runtime. This is why the called method (optionally) receives
29 * the priority of a processor as second argument.
31 * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
33 class TaggedHandlersPass implements CompilerPassInterface {
38 * Finds services tagged with 'service_collector', then finds all
39 * corresponding tagged services and adds a method call for each to the
40 * consuming/collecting service definition.
42 * Supported 'service_collector' tag attributes:
43 * - tag: The tag name used by handler services to collect. Defaults to the
44 * service ID of the consumer.
45 * - call: The method name to call on the consumer service. Defaults to
46 * 'addHandler'. The called method receives two arguments:
47 * - The handler instance as first argument.
48 * - Optionally the handler's priority as second argument, if the method
49 * accepts a second parameter and its name is "priority". In any case, all
50 * handlers registered at compile time are sorted already.
51 * - required: Boolean indicating if at least one handler service is required.
57 * - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
60 * Supported handler tag attributes:
61 * - priority: An integer denoting the priority of the handler. Defaults to 0.
66 * - { name: breadcrumb_builder, priority: 100 }
69 * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
70 * If the method of a consumer service to be called does not type-hint an
72 * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
73 * If a tagged handler does not implement the required interface.
74 * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
75 * If at least one tagged service is required but none are found.
77 public function process(ContainerBuilder $container) {
78 foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) {
79 foreach ($passes as $pass) {
81 $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
82 $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
83 $required = isset($pass['required']) ? $pass['required'] : FALSE;
85 // Determine parameters.
86 $consumer = $container->getDefinition($consumer_id);
87 $method = new \ReflectionMethod($consumer->getClass(), $method_name);
88 $params = $method->getParameters();
94 foreach ($params as $pos => $param) {
95 if ($param->getClass()) {
96 $interface = $param->getClass();
98 elseif ($param->getName() === 'id') {
101 elseif ($param->getName() === 'priority') {
102 $priority_pos = $pos;
105 $extra_params[$param->getName()] = $pos;
110 if (!isset($interface)) {
111 throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
113 $consumer->getClass(),
117 $interface = $interface->getName();
119 // Find all tagged handlers.
121 $extra_arguments = [];
122 foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
123 // Validate the interface.
124 $handler = $container->getDefinition($id);
125 if (!is_subclass_of($handler->getClass(), $interface)) {
126 throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
128 $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
129 // Keep track of other tagged handlers arguments.
130 foreach ($extra_params as $name => $pos) {
131 $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
134 if (empty($handlers)) {
136 throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
140 // Sort all handlers by priority.
141 arsort($handlers, SORT_NUMERIC);
143 // Add a method call for each handler to the consumer service
145 foreach ($handlers as $id => $priority) {
147 $arguments[$interface_pos] = new Reference($id);
148 if (isset($priority_pos)) {
149 $arguments[$priority_pos] = $priority;
151 if (isset($id_pos)) {
152 $arguments[$id_pos] = $id;
154 // Add in extra arguments.
155 if (isset($extra_arguments[$id])) {
156 // Place extra arguments in their right positions.
157 $arguments += $extra_arguments[$id];
159 // Sort the arguments by position.
161 $consumer->addMethodCall($method_name, $arguments);