TaggedHandlersPass.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
  2. 8.9.x core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
  3. 11.x core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php

Namespace

Drupal\Core\DependencyInjection\Compiler

File

core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php

View source
<?php

namespace Drupal\Core\DependencyInjection\Compiler;

use Drupal\Component\Utility\Reflection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
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 or just
 * their IDs injected, in order to establish an extensible architecture.
 *
 * 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.
 *
 * 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.
 *
 * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
 */
class TaggedHandlersPass implements CompilerPassInterface {
    
    /**
     * Service tag information keyed by tag name.
     *
     * @var array
     */
    protected $tagCache = [];
    
    /**
     * {@inheritdoc}
     *
     * 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.
     *
     * 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.
     * - 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 at least one argument,
     *     optionally more:
     *     - The handler instance must be the first method parameter, and it must
     *       have a type declaration.
     *     - If the method has a parameter named $id, in any position, it will
     *       receive the value of service ID when called.
     *     - If the method has a parameter named $priority, in any position, it
     *       will receive the value of the tag's 'priority' attribute.
     *     - Any other method parameters whose names match the name of an
     *       attribute of the tag will receive the value of that tag attribute.The
     *       order of the method parameters and the order of the service tag
     *       attributes do not need to match.
     *
     * Example (YAML):
     * @code
     * tags:
     *   - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
     *   - { name: service_id_collector, tag: theme_negotiator }
     * @endcode
     *
     * Supported handler tag attributes:
     * - priority: An integer denoting the priority of the handler. Defaults to 0.
     *
     * Example (YAML):
     * @code
     * tags:
     *   - { name: breadcrumb_builder, priority: 100 }
     * @endcode
     *
     * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
     *   If the method of a consumer service to be called does not type-hint an
     *   interface.
     * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
     *   If a tagged handler does not implement the required interface.
     * @throws \Symfony\Component\DependencyInjection\Exception\LogicException
     *   If at least one tagged service is required but none are found.
     * phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
     * @return void
     */
    public function process(ContainerBuilder $container) {
        // Avoid using ContainerBuilder::findTaggedServiceIds() as that results in
        // additional iterations around all the service definitions.
        foreach ($container->getDefinitions() as $id => $definition) {
            foreach ($definition->getTags() as $name => $info) {
                $this->tagCache[$name][$id] = $info;
            }
        }
        foreach ($this->tagCache['service_collector'] ?? [] as $consumer_id => $tags) {
            foreach ($tags as $pass) {
                $this->processServiceCollectorPass($pass, $consumer_id, $container);
            }
        }
        foreach ($this->tagCache['service_id_collector'] ?? [] as $consumer_id => $tags) {
            foreach ($tags 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 = $pass['tag'] ?? $consumer_id;
        $method_name = $pass['call'] ?? 'addHandler';
        $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) {
            $class = Reflection::getParameterClassName($param);
            if ($class !== NULL) {
                $interface = $class;
            }
            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,
            ]));
        }
        // Find all tagged handlers.
        $handlers = [];
        $extra_arguments = [];
        foreach ($this->tagCache[$tag] ?? [] as $id => $attributes) {
            // Validate the interface.
            $handler = $container->getDefinition($id);
            if (!is_a($handler->getClass(), $interface, TRUE)) {
                throw new LogicException("Service '{$id}' for consumer '{$consumer_id}' does not implement {$interface}.");
            }
            $handlers[$id] = $attributes[0]['priority'] ?? 0;
            // Keep track of other tagged handlers arguments.
            foreach ($extra_params as $name => $pos) {
                $extra_arguments[$id][$pos] = $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 = $pass['tag'] ?? $consumer_id;
        $required = $pass['required'] ?? FALSE;
        $consumer = $container->getDefinition($consumer_id);
        // Find all tagged handlers.
        $handlers = [];
        foreach ($this->tagCache[$tag] ?? [] as $id => $attributes) {
            $handlers[$id] = $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));
    }

}

Classes

Title Deprecated Summary
TaggedHandlersPass Collects services to add/inject them into a consumer service.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.