Drupal Service Container Deep Dive (Part 3): Service Collectors

This article is the third part of a series exploring the Drupal service container, focusing on service collectors.
Drupal Service Container Deep Dive (Part 3): Service Collectors

In the first part and second part of this series, we explored the basics of the Drupal service container, including tags, compiler passes, service providers, autoconfiguration, aliases, autowiring, and named arguments. In this third part, we examine service collectors, which aggregate multiple services into a single service for streamlined access.

This article was originally published on the SparkFabrik engineering blog and is republished here with permission.

Service Collectors

When discussing service tags, we noted that they attach metadata to services and require a compiler pass for processing.

Drupal provides a shortcut for a common tagging use case through service collectors. A service collector automatically gathers all services with a specific tag and injects them into another service.

For example, adding a Twig extension requires tagging a service with twig.extension:

services:
  my_module.my_twig_extension:
    class: Drupal\my_module\Twig\MyTwigExtension
    tags:
      - { name: twig.extension }

The twig service collector automatically collects these tagged services. Its definition in core.services.yml looks like this:

twig:
  class: Drupal\Core\Template\TwigEnvironment
  arguments:
    - '%app.root%'
    - '@cache.default'
    - '%twig_extension_hash%'
    - '@state'
    - '@twig.loader'
    - '%twig.config%'
  tags:
    - { name: service_collector, tag: 'twig.extension', call: addExtension }

Each collected service is passed to the addExtension method:

public function addExtension(ExtensionInterface $extension): void {
  $this->extensionSet->addExtension($extension);
}

If the call attribute is omitted, Drupal defaults to a method named addHandler. If the tag attribute is omitted, the service name itself is used as the tag.

Advanced Usage

You can control execution order using a priority attribute:

services:
  my_module.first_twig_extension:
    class: Drupal\my_module\Twig\FirstTwigExtension
    tags:
      - { name: twig.extension, priority: 10 }
  my_module.second_twig_extension:
    class: Drupal\my_module\Twig\SecondTwigExtension
    tags:
      - { name: twig.extension, priority: 5 }

To access the service ID when collecting, use index_by: id:

my_module.custom_collector:
  class: Drupal\my_module\CustomCollector
  tags:
    - { name: service_collector, tag: custom.tag, call: addService, index_by: id }

Additional attributes are passed as method arguments:

my_module.custom_collector:
  class: Drupal\my_module\CustomCollector
  tags:
    - { name: service_collector, tag: custom.tag, call: addService, custom_arg: 'value' }
public function addService(MyService $service, string $custom_arg): void {
  // Custom logic.
}

Required collectors can be enforced with required: true:

twig.loader:
  class: Twig\Loader\ChainLoader
  public: false
  tags:
    - { name: service_collector, tag: twig.loader, call: addLoader, required: true }

Service ID Collectors

Service collectors instantiate all tagged services eagerly, which can affect performance. Drupal offers service ID collectors to defer instantiation.

theme.negotiator:
  class: Drupal\Core\Theme\ThemeNegotiator
  arguments:
    - '@access_check.theme'
    - '@class_resolver'
  tags:
    - { name: service_id_collector, tag: theme_negotiator }
public function __construct(
  ThemeAccessCheck $theme_access,
  ClassResolverInterface $class_resolver,
  array $negotiators
) {
  // ...
}
public function determineActiveTheme(RouteMatchInterface $route_match): void {
  foreach ($this->negotiators as $negotiator_id) {
    $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
  }
}

As discussed in Part 2, service locators are generally considered an anti-pattern.

AutowireIterator

Since Symfony 7.0 (and Drupal 11), developers can use AutowireIterator for lazy service injection.

public function __construct(
  #[AutowireIterator(tag: 'plugin_manager_cache_clear')]
  protected \Traversable $cachedDiscoveries,
) {}

This approach avoids eager instantiation and removes the need for service locators.

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(index: 'handler_one', priority: 10)]
class One {
  // ...
}

Note: These attributes require autowiring: true in the module’s .services.yml file.

Conclusion

Service collectors, service ID collectors, and AutowireIterator provide different trade-offs between flexibility and performance. Modern Drupal increasingly aligns with Symfony’s container features, reducing the need for Drupal-specific patterns.

  • Part 1: service tags and compiler passes
  • Part 2: autowiring and aliases
  • Part 4: factories
  • Part 5: backend overrides
  • Part 6: service decoration and lazy loading

Note: The vision of this web portal is to help promote news and stories around the Drupal community and promote and celebrate the people and organizations in the community. We strive to create and distribute our content based on these content policy. If you see any omission/variation on this please reach out to us at #thedroptimes channel on Drupal Slack and we will try to address the issue as best we can.

Related Organizations

Upcoming Events

Latest Opportunities