Version 1
[yaffs-website] / web / core / lib / Drupal / Core / DependencyInjection / Compiler / TaggedHandlersPass.php
1 <?php
2
3 namespace Drupal\Core\DependencyInjection\Compiler;
4
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;
9
10 /**
11  * Collects services to add/inject them into a consumer service.
12  *
13  * This mechanism allows a service to get multiple processor services injected,
14  * in order to establish an extensible architecture.
15  *
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.
20  *
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.
25  *
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.
30  *
31  * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
32  */
33 class TaggedHandlersPass implements CompilerPassInterface {
34
35   /**
36    * {@inheritdoc}
37    *
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.
41    *
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.
52    *   Defaults to FALSE.
53    *
54    * Example (YAML):
55    * @code
56    * tags:
57    *   - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
58    * @endcode
59    *
60    * Supported handler tag attributes:
61    * - priority: An integer denoting the priority of the handler. Defaults to 0.
62    *
63    * Example (YAML):
64    * @code
65    * tags:
66    *   - { name: breadcrumb_builder, priority: 100 }
67    * @endcode
68    *
69    * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
70    *   If the method of a consumer service to be called does not type-hint an
71    *   interface.
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.
76    */
77   public function process(ContainerBuilder $container) {
78     foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) {
79       foreach ($passes as $pass) {
80         $interface = NULL;
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;
84
85         // Determine parameters.
86         $consumer = $container->getDefinition($consumer_id);
87         $method = new \ReflectionMethod($consumer->getClass(), $method_name);
88         $params = $method->getParameters();
89
90         $interface_pos = 0;
91         $id_pos = NULL;
92         $priority_pos = NULL;
93         $extra_params = [];
94         foreach ($params as $pos => $param) {
95           if ($param->getClass()) {
96             $interface = $param->getClass();
97           }
98           elseif ($param->getName() === 'id') {
99             $id_pos = $pos;
100           }
101           elseif ($param->getName() === 'priority') {
102             $priority_pos = $pos;
103           }
104           else {
105             $extra_params[$param->getName()] = $pos;
106           }
107         }
108         // Determine the ID.
109
110         if (!isset($interface)) {
111           throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
112             $consumer_id,
113             $consumer->getClass(),
114             $method_name,
115           ]));
116         }
117         $interface = $interface->getName();
118
119         // Find all tagged handlers.
120         $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.");
127           }
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();
132           }
133         }
134         if (empty($handlers)) {
135           if ($required) {
136             throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
137           }
138           continue;
139         }
140         // Sort all handlers by priority.
141         arsort($handlers, SORT_NUMERIC);
142
143         // Add a method call for each handler to the consumer service
144         // definition.
145         foreach ($handlers as $id => $priority) {
146           $arguments = [];
147           $arguments[$interface_pos] = new Reference($id);
148           if (isset($priority_pos)) {
149             $arguments[$priority_pos] = $priority;
150           }
151           if (isset($id_pos)) {
152             $arguments[$id_pos] = $id;
153           }
154           // Add in extra arguments.
155           if (isset($extra_arguments[$id])) {
156             // Place extra arguments in their right positions.
157             $arguments += $extra_arguments[$id];
158           }
159           // Sort the arguments by position.
160           ksort($arguments);
161           $consumer->addMethodCall($method_name, $arguments);
162         }
163       }
164     }
165   }
166
167 }