TaggedHandlersPass.php 8.9 KB

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