Security update for Core, with self-updated composer
[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 or just
14  * their IDs injected, in order to establish an extensible architecture.
15  *
16  * The service collector differs from the factory pattern in that processors are
17  * not lazily instantiated on demand; the consuming service receives instances
18  * of all registered processors when it is instantiated. Unlike a factory
19  * service, the consuming service is not ContainerAware. It differs from regular
20  * service definition arguments (constructor injection) in that a consuming
21  * service MAY allow further processors to be added dynamically at runtime. This
22  * is why the called method (optionally) receives the priority of a processor as
23  * second argument.
24  *
25  * To lazily instantiate services the service ID collector pattern can be used,
26  * but the consumer service needs to also inject the 'class_resolver' service.
27  * As constructor injection is used, processors cannot be added at runtime via
28  * this method. However, a consuming service could have setter methods to allow
29  * runtime additions.
30  *
31  * These differ from plugins in that all processors are explicitly registered by
32  * service providers (driven by declarative configuration in code); the mere
33  * availability of a processor (cf. plugin discovery) does not imply that a
34  * processor ought to be registered and used.
35  *
36  * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
37  */
38 class TaggedHandlersPass implements CompilerPassInterface {
39
40   /**
41    * {@inheritdoc}
42    *
43    * Finds services tagged with 'service_collector' or 'service_id_collector',
44    * then finds all corresponding tagged services.
45    *
46    * The service collector adds a method call for each to the
47    * consuming/collecting service definition.
48    *
49    * The service ID collector will collect an array of service IDs and add them
50    * as a constructor argument.
51    *
52    * Supported tag attributes:
53    * - tag: The tag name used by handler services to collect. Defaults to the
54    *   service ID of the consumer.
55    * - required: Boolean indicating if at least one handler service is required.
56    *   Defaults to FALSE.
57    *
58    * Additional tag attributes supported by 'service_collector' only:
59    *   - call: The method name to call on the consumer service. Defaults to
60    *     'addHandler'. The called method receives two arguments:
61    *     - The handler instance as first argument.
62    *     - Optionally the handler's priority as second argument, if the method
63    *       accepts a second parameter and its name is "priority". In any case,
64    *       all handlers registered at compile time are sorted already.
65    *
66    * Example (YAML):
67    * @code
68    * tags:
69    *   - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
70    *   - { name: service_id_collector, tag: theme_negotiator }
71    * @endcode
72    *
73    * Supported handler tag attributes:
74    * - priority: An integer denoting the priority of the handler. Defaults to 0.
75    *
76    * Example (YAML):
77    * @code
78    * tags:
79    *   - { name: breadcrumb_builder, priority: 100 }
80    * @endcode
81    *
82    * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
83    *   If the method of a consumer service to be called does not type-hint an
84    *   interface.
85    * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
86    *   If a tagged handler does not implement the required interface.
87    * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
88    *   If at least one tagged service is required but none are found.
89    */
90   public function process(ContainerBuilder $container) {
91     // Avoid using ContainerBuilder::findTaggedServiceIds() as that we result in
92     // additional iterations around all the service definitions.
93     foreach ($container->getDefinitions() as $consumer_id => $definition) {
94       $tags = $definition->getTags();
95       if (isset($tags['service_collector'])) {
96         foreach ($tags['service_collector'] as $pass) {
97           $this->processServiceCollectorPass($pass, $consumer_id, $container);
98         }
99       }
100       if (isset($tags['service_id_collector'])) {
101         foreach ($tags['service_id_collector'] as $pass) {
102           $this->processServiceIdCollectorPass($pass, $consumer_id, $container);
103         }
104       }
105     }
106   }
107
108   /**
109    * Processes a service collector service pass.
110    *
111    * @param array $pass
112    *   The service collector pass data.
113    * @param string $consumer_id
114    *   The consumer service ID.
115    * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
116    *   The service container.
117    */
118   protected function processServiceCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
119     $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
120     $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
121     $required = isset($pass['required']) ? $pass['required'] : FALSE;
122
123     // Determine parameters.
124     $consumer = $container->getDefinition($consumer_id);
125     $method = new \ReflectionMethod($consumer->getClass(), $method_name);
126     $params = $method->getParameters();
127
128     $interface_pos = 0;
129     $id_pos = NULL;
130     $priority_pos = NULL;
131     $extra_params = [];
132     foreach ($params as $pos => $param) {
133       if ($param->getClass()) {
134         $interface = $param->getClass();
135       }
136       elseif ($param->getName() === 'id') {
137         $id_pos = $pos;
138       }
139       elseif ($param->getName() === 'priority') {
140         $priority_pos = $pos;
141       }
142       else {
143         $extra_params[$param->getName()] = $pos;
144       }
145     }
146     // Determine the ID.
147
148     if (!isset($interface)) {
149       throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
150         $consumer_id,
151         $consumer->getClass(),
152         $method_name,
153       ]));
154     }
155     $interface = $interface->getName();
156
157     // Find all tagged handlers.
158     $handlers = [];
159     $extra_arguments = [];
160     foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
161       // Validate the interface.
162       $handler = $container->getDefinition($id);
163       if (!is_subclass_of($handler->getClass(), $interface)) {
164         throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
165       }
166       $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
167       // Keep track of other tagged handlers arguments.
168       foreach ($extra_params as $name => $pos) {
169         $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
170       }
171     }
172
173     if ($required && empty($handlers)) {
174       throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
175     }
176
177     // Sort all handlers by priority.
178     arsort($handlers, SORT_NUMERIC);
179
180     // Add a method call for each handler to the consumer service
181     // definition.
182     foreach ($handlers as $id => $priority) {
183       $arguments = [];
184       $arguments[$interface_pos] = new Reference($id);
185       if (isset($priority_pos)) {
186         $arguments[$priority_pos] = $priority;
187       }
188       if (isset($id_pos)) {
189         $arguments[$id_pos] = $id;
190       }
191       // Add in extra arguments.
192       if (isset($extra_arguments[$id])) {
193         // Place extra arguments in their right positions.
194         $arguments += $extra_arguments[$id];
195       }
196       // Sort the arguments by position.
197       ksort($arguments);
198       $consumer->addMethodCall($method_name, $arguments);
199     }
200   }
201
202   /**
203    * Processes a service collector ID service pass.
204    *
205    * @param array $pass
206    *   The service collector pass data.
207    * @param string $consumer_id
208    *   The consumer service ID.
209    * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
210    *   The service container.
211    */
212   protected function processServiceIdCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
213     $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
214     $required = isset($pass['required']) ? $pass['required'] : FALSE;
215
216     $consumer = $container->getDefinition($consumer_id);
217
218     // Find all tagged handlers.
219     $handlers = [];
220     foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
221       $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
222     }
223
224     if ($required && empty($handlers)) {
225       throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
226     }
227
228     // Sort all handlers by priority.
229     arsort($handlers, SORT_NUMERIC);
230
231     $consumer->addArgument(array_keys($handlers));
232   }
233
234 }