Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / DependencyInjection / Compiler / TaggedHandlersPass.php
index bb87d5e484235b65b11273d5ecc9ef3b144028e4..35b5beaaa37a18245c46c758ac214b10279fd015 100644 (file)
@@ -10,24 +10,29 @@ use Symfony\Component\DependencyInjection\Reference;
 /**
  * Collects services to add/inject them into a consumer service.
  *
- * This mechanism allows a service to get multiple processor services injected,
- * in order to establish an extensible architecture.
+ * This mechanism allows a service to get multiple processor services or just
+ * their IDs injected, in order to establish an extensible architecture.
  *
- * It differs from the factory pattern in that processors are not lazily
- * instantiated on demand; the consuming service receives instances of all
- * registered processors when it is instantiated. Unlike a factory service, the
- * consuming service is not ContainerAware.
+ * The service collector differs from the factory pattern in that processors are
+ * not lazily instantiated on demand; the consuming service receives instances
+ * of all registered processors when it is instantiated. Unlike a factory
+ * service, the consuming service is not ContainerAware. It differs from regular
+ * service definition arguments (constructor injection) in that a consuming
+ * service MAY allow further processors to be added dynamically at runtime. This
+ * is why the called method (optionally) receives the priority of a processor as
+ * second argument.
  *
- * It differs from plugins in that all processors are explicitly registered by
+ * To lazily instantiate services the service ID collector pattern can be used,
+ * but the consumer service needs to also inject the 'class_resolver' service.
+ * As constructor injection is used, processors cannot be added at runtime via
+ * this method. However, a consuming service could have setter methods to allow
+ * runtime additions.
+ *
+ * These differ from plugins in that all processors are explicitly registered by
  * service providers (driven by declarative configuration in code); the mere
  * availability of a processor (cf. plugin discovery) does not imply that a
  * processor ought to be registered and used.
  *
- * It differs from regular service definition arguments (constructor injection)
- * in that a consuming service MAY allow further processors to be added
- * dynamically at runtime. This is why the called method (optionally) receives
- * the priority of a processor as second argument.
- *
  * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
  */
 class TaggedHandlersPass implements CompilerPassInterface {
@@ -35,26 +40,34 @@ class TaggedHandlersPass implements CompilerPassInterface {
   /**
    * {@inheritdoc}
    *
-   * Finds services tagged with 'service_collector', then finds all
-   * corresponding tagged services and adds a method call for each to the
+   * Finds services tagged with 'service_collector' or 'service_id_collector',
+   * then finds all corresponding tagged services.
+   *
+   * The service collector adds a method call for each to the
    * consuming/collecting service definition.
    *
-   * Supported 'service_collector' tag attributes:
+   * The service ID collector will collect an array of service IDs and add them
+   * as a constructor argument.
+   *
+   * Supported tag attributes:
    * - tag: The tag name used by handler services to collect. Defaults to the
    *   service ID of the consumer.
-   * - call: The method name to call on the consumer service. Defaults to
-   *   'addHandler'. The called method receives two arguments:
-   *   - The handler instance as first argument.
-   *   - Optionally the handler's priority as second argument, if the method
-   *     accepts a second parameter and its name is "priority". In any case, all
-   *     handlers registered at compile time are sorted already.
    * - required: Boolean indicating if at least one handler service is required.
    *   Defaults to FALSE.
    *
+   * Additional tag attributes supported by 'service_collector' only:
+   *   - call: The method name to call on the consumer service. Defaults to
+   *     'addHandler'. The called method receives two arguments:
+   *     - The handler instance as first argument.
+   *     - Optionally the handler's priority as second argument, if the method
+   *       accepts a second parameter and its name is "priority". In any case,
+   *       all handlers registered at compile time are sorted already.
+   *
    * Example (YAML):
    * @code
    * tags:
    *   - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
+   *   - { name: service_id_collector, tag: theme_negotiator }
    * @endcode
    *
    * Supported handler tag attributes:
@@ -75,93 +88,147 @@ class TaggedHandlersPass implements CompilerPassInterface {
    *   If at least one tagged service is required but none are found.
    */
   public function process(ContainerBuilder $container) {
-    foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) {
-      foreach ($passes as $pass) {
-        $interface = NULL;
-        $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
-        $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
-        $required = isset($pass['required']) ? $pass['required'] : FALSE;
-
-        // Determine parameters.
-        $consumer = $container->getDefinition($consumer_id);
-        $method = new \ReflectionMethod($consumer->getClass(), $method_name);
-        $params = $method->getParameters();
-
-        $interface_pos = 0;
-        $id_pos = NULL;
-        $priority_pos = NULL;
-        $extra_params = [];
-        foreach ($params as $pos => $param) {
-          if ($param->getClass()) {
-            $interface = $param->getClass();
-          }
-          elseif ($param->getName() === 'id') {
-            $id_pos = $pos;
-          }
-          elseif ($param->getName() === 'priority') {
-            $priority_pos = $pos;
-          }
-          else {
-            $extra_params[$param->getName()] = $pos;
-          }
-        }
-        // Determine the ID.
-
-        if (!isset($interface)) {
-          throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
-            $consumer_id,
-            $consumer->getClass(),
-            $method_name,
-          ]));
+    // Avoid using ContainerBuilder::findTaggedServiceIds() as that we result in
+    // additional iterations around all the service definitions.
+    foreach ($container->getDefinitions() as $consumer_id => $definition) {
+      $tags = $definition->getTags();
+      if (isset($tags['service_collector'])) {
+        foreach ($tags['service_collector'] as $pass) {
+          $this->processServiceCollectorPass($pass, $consumer_id, $container);
         }
-        $interface = $interface->getName();
-
-        // Find all tagged handlers.
-        $handlers = [];
-        $extra_arguments = [];
-        foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
-          // Validate the interface.
-          $handler = $container->getDefinition($id);
-          if (!is_subclass_of($handler->getClass(), $interface)) {
-            throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
-          }
-          $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
-          // Keep track of other tagged handlers arguments.
-          foreach ($extra_params as $name => $pos) {
-            $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
-          }
-        }
-        if (empty($handlers)) {
-          if ($required) {
-            throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
-          }
-          continue;
-        }
-        // Sort all handlers by priority.
-        arsort($handlers, SORT_NUMERIC);
-
-        // Add a method call for each handler to the consumer service
-        // definition.
-        foreach ($handlers as $id => $priority) {
-          $arguments = [];
-          $arguments[$interface_pos] = new Reference($id);
-          if (isset($priority_pos)) {
-            $arguments[$priority_pos] = $priority;
-          }
-          if (isset($id_pos)) {
-            $arguments[$id_pos] = $id;
-          }
-          // Add in extra arguments.
-          if (isset($extra_arguments[$id])) {
-            // Place extra arguments in their right positions.
-            $arguments += $extra_arguments[$id];
-          }
-          // Sort the arguments by position.
-          ksort($arguments);
-          $consumer->addMethodCall($method_name, $arguments);
+      }
+      if (isset($tags['service_id_collector'])) {
+        foreach ($tags['service_id_collector'] as $pass) {
+          $this->processServiceIdCollectorPass($pass, $consumer_id, $container);
         }
       }
     }
   }
 
+  /**
+   * Processes a service collector service pass.
+   *
+   * @param array $pass
+   *   The service collector pass data.
+   * @param string $consumer_id
+   *   The consumer service ID.
+   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+   *   The service container.
+   */
+  protected function processServiceCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
+    $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
+    $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
+    $required = isset($pass['required']) ? $pass['required'] : FALSE;
+
+    // Determine parameters.
+    $consumer = $container->getDefinition($consumer_id);
+    $method = new \ReflectionMethod($consumer->getClass(), $method_name);
+    $params = $method->getParameters();
+
+    $interface_pos = 0;
+    $id_pos = NULL;
+    $priority_pos = NULL;
+    $extra_params = [];
+    foreach ($params as $pos => $param) {
+      if ($param->getClass()) {
+        $interface = $param->getClass();
+      }
+      elseif ($param->getName() === 'id') {
+        $id_pos = $pos;
+      }
+      elseif ($param->getName() === 'priority') {
+        $priority_pos = $pos;
+      }
+      else {
+        $extra_params[$param->getName()] = $pos;
+      }
+    }
+    // Determine the ID.
+
+    if (!isset($interface)) {
+      throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
+        $consumer_id,
+        $consumer->getClass(),
+        $method_name,
+      ]));
+    }
+    $interface = $interface->getName();
+
+    // Find all tagged handlers.
+    $handlers = [];
+    $extra_arguments = [];
+    foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
+      // Validate the interface.
+      $handler = $container->getDefinition($id);
+      if (!is_subclass_of($handler->getClass(), $interface)) {
+        throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
+      }
+      $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+      // Keep track of other tagged handlers arguments.
+      foreach ($extra_params as $name => $pos) {
+        $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
+      }
+    }
+
+    if ($required && empty($handlers)) {
+      throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
+    }
+
+    // Sort all handlers by priority.
+    arsort($handlers, SORT_NUMERIC);
+
+    // Add a method call for each handler to the consumer service
+    // definition.
+    foreach ($handlers as $id => $priority) {
+      $arguments = [];
+      $arguments[$interface_pos] = new Reference($id);
+      if (isset($priority_pos)) {
+        $arguments[$priority_pos] = $priority;
+      }
+      if (isset($id_pos)) {
+        $arguments[$id_pos] = $id;
+      }
+      // Add in extra arguments.
+      if (isset($extra_arguments[$id])) {
+        // Place extra arguments in their right positions.
+        $arguments += $extra_arguments[$id];
+      }
+      // Sort the arguments by position.
+      ksort($arguments);
+      $consumer->addMethodCall($method_name, $arguments);
+    }
+  }
+
+  /**
+   * Processes a service collector ID service pass.
+   *
+   * @param array $pass
+   *   The service collector pass data.
+   * @param string $consumer_id
+   *   The consumer service ID.
+   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+   *   The service container.
+   */
+  protected function processServiceIdCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
+    $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
+    $required = isset($pass['required']) ? $pass['required'] : FALSE;
+
+    $consumer = $container->getDefinition($consumer_id);
+
+    // Find all tagged handlers.
+    $handlers = [];
+    foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
+      $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+    }
+
+    if ($required && empty($handlers)) {
+      throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
+    }
+
+    // Sort all handlers by priority.
+    arsort($handlers, SORT_NUMERIC);
+
+    $consumer->addArgument(array_keys($handlers));
+  }
+
 }