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 or just
14 * their IDs injected, in order to establish an extensible architecture.
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
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
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.
36 * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
38 class TaggedHandlersPass implements CompilerPassInterface {
43 * Finds services tagged with 'service_collector' or 'service_id_collector',
44 * then finds all corresponding tagged services.
46 * The service collector adds a method call for each to the
47 * consuming/collecting service definition.
49 * The service ID collector will collect an array of service IDs and add them
50 * as a constructor argument.
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.
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.
69 * - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
70 * - { name: service_id_collector, tag: theme_negotiator }
73 * Supported handler tag attributes:
74 * - priority: An integer denoting the priority of the handler. Defaults to 0.
79 * - { name: breadcrumb_builder, priority: 100 }
82 * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
83 * If the method of a consumer service to be called does not type-hint an
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.
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);
100 if (isset($tags['service_id_collector'])) {
101 foreach ($tags['service_id_collector'] as $pass) {
102 $this->processServiceIdCollectorPass($pass, $consumer_id, $container);
109 * Processes a service collector service 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.
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;
123 // Determine parameters.
124 $consumer = $container->getDefinition($consumer_id);
125 $method = new \ReflectionMethod($consumer->getClass(), $method_name);
126 $params = $method->getParameters();
130 $priority_pos = NULL;
132 foreach ($params as $pos => $param) {
133 if ($param->getClass()) {
134 $interface = $param->getClass();
136 elseif ($param->getName() === 'id') {
139 elseif ($param->getName() === 'priority') {
140 $priority_pos = $pos;
143 $extra_params[$param->getName()] = $pos;
148 if (!isset($interface)) {
149 throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
151 $consumer->getClass(),
155 $interface = $interface->getName();
157 // Find all tagged 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.");
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();
173 if ($required && empty($handlers)) {
174 throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
177 // Sort all handlers by priority.
178 arsort($handlers, SORT_NUMERIC);
180 // Add a method call for each handler to the consumer service
182 foreach ($handlers as $id => $priority) {
184 $arguments[$interface_pos] = new Reference($id);
185 if (isset($priority_pos)) {
186 $arguments[$priority_pos] = $priority;
188 if (isset($id_pos)) {
189 $arguments[$id_pos] = $id;
191 // Add in extra arguments.
192 if (isset($extra_arguments[$id])) {
193 // Place extra arguments in their right positions.
194 $arguments += $extra_arguments[$id];
196 // Sort the arguments by position.
198 $consumer->addMethodCall($method_name, $arguments);
203 * Processes a service collector ID service 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.
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;
216 $consumer = $container->getDefinition($consumer_id);
218 // Find all tagged handlers.
220 foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
221 $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
224 if ($required && empty($handlers)) {
225 throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
228 // Sort all handlers by priority.
229 arsort($handlers, SORT_NUMERIC);
231 $consumer->addArgument(array_keys($handlers));