---
title: "Drupal service container deep dive (Part 3): service collectors"
url: "https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-3/"
lang: "en"
type: "engineering-post"
date: "2026-02-05"
lastmod: "2026-02-05"
author: "Luca Lusso"
description: "In this third part of our deep dive into the Drupal service container, we explore service collectors, which aggregate multiple services into a single service for streamlined access."
tags: ["drupal","php","dependency injection","autowiring","service container"]
image: "https://www.sparkfabrik.com/images/og/engineering-drupal-service-container-deep-dive-part-3-en.webp"
schema: {"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Luca Lusso"},"dateModified":"2026-02-05T09:00:00+00:00","datePublished":"2026-02-05T09:00:00+00:00","description":"In this third part of our deep dive into the Drupal service container, we explore service collectors, which aggregate multiple services into a single service for streamlined access.","headline":"Drupal service container deep dive (Part 3): service collectors","image":"https://www.sparkfabrik.com/images/og/engineering-drupal-service-container-deep-dive-part-3-en.webp","inLanguage":"en","publisher":{"@type":"Organization","name":"SparkFabrik","url":"https://www.sparkfabrik.com"},"url":"https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-3/"}
---

# Drupal service container deep dive (Part 3): service collectors

In this third part of our deep dive into the Drupal service container, we explore service collectors, which aggregate multiple services into a single service for streamlined access.

- Author: [Luca Lusso](https://www.sparkfabrik.com/en/engineering/authors/llusso/)
- Published: 2026-02-05
- Topic: [Software development](https://www.sparkfabrik.com/en/engineering/families/dev/)
- Tags: drupal, php, dependency injection, autowiring, service container

![Drupal Service Container deep dive, part 3](https://www.sparkfabrik.com/images/engineering/legacy/content/drupal-service-container/drupal_service_container-p3.png)
*Drupal Service Container deep dive, part 3*

In the [first part](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-1/) and [second part](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-2/) of this series, we explored the basics of the Drupal service container like **tags**, **compiler passes**, **service providers**, **autoconfiguration**, **aliases**, **autowiring**, and **named arguments**. In this third part, we will explore **service collectors**, which aggregate multiple services into a single service for streamlined access.

## Service Collectors

When we talked about [service tags](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-1/#service-tags), we mentioned that they are a way to add metadata to services in the service container, and that we need to write a compiler pass to process them.

Drupal provides a shortcut for a common use case of service tags called **service collectors**. A service collector can be used to automatically collect all services tagged with a specific tag and inject them into another service. This is particularly useful when you have a set of related services that you want to manage collectively.

For example, every time you want to add a new Twig extension in Drupal, you must tag your service with the `twig.extension` tag, like this:

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

The `twig` service collector will automatically gather all services with this tag and add them to the Twig environment. Here’s how the `twig` service is defined in `core.services.yml` using a service collector:

```yml
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 }
```

In this example, the `twig` service is defined with a `service_collector` tag. This tag specifies that all services tagged with `twig.extension` should be collected and added to the Twig environment using the `addExtension` method.

In the `TwigEnvironment` class, the `addExtension` method is called for each collected service:

```php
public function addExtension(ExtensionInterface $extension)
{
    $this->extensionSet->addExtension($extension);
    [...]
}
```

The `call` attribute in the `service_collector` tag can be omitted, in which case a default method named `addHandler` will be used. In the same way, if the `tag` attribute is omitted, the name of the service being defined (in this case `twig`) will be used as the tag name to collect services.

### Advanced Usage

If you need to add tagged services to a service collector in a specific order, you can use the `priority` attribute in the tag definition. Services with higher priority values will be added first. For example:

```yml
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 }
```

If you need to know the id of each tagged service when adding it to the collector, you can use the `id` attribute. This will pass the service id as an additional argument to the specified method:

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

Finally, every other attribute you add to the `service_collector` tag will be passed as additional arguments to the specified method. For example, if you want to pass a custom argument along with each tagged service, you can do so like this:

```yml
my_module.custom_collector:
  class: Drupal\my_module\CustomCollector
  tags:
    - { name: service_collector, tag: custom.tag, call: addService, custom_arg: 'value' }
```

The code in the `CustomCollector` class would look like this:

```php
public function addService(MyService $service, string $custom_arg): void {
    // Your logic here
}
```

The order of the arguments in the tag and in the method signature is not important, as long as the names match.

If your service collector needs at least one tagged service to function correctly, you can enforce this requirement by adding the `required: true` attribute to the `service_collector` tag:

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

This ensures that if no services are tagged with `twig.loader`, the service container will throw an error, preventing potential runtime issues.

Injecting services into a service collector is an heavy operation because the service container needs to create an instance for all the tagged services before it can inject them into the collector. A better approach is to use a `service_id_collector`, that collects the service ids instead of the actual services.

## Service ID Collectors

When dealing with a large number of services, injecting all of them into a service collector can lead to performance issues, as the service container has to instantiate each service before injecting it. To mitigate this, Drupal provides the concept of **service ID collectors**. With service id collectors, the services are only created when they are actually needed, like in:

```yml
theme.negotiator:
  class: Drupal\Core\Theme\ThemeNegotiator
  arguments: ['@access_check.theme', '@class_resolver']
  tags:
    - { name: service_id_collector, tag: theme_negotiator }
```

The list of service ids is injected into the `ThemeNegotiator` service using constructor injection:

```php
/**
 * Constructs a new ThemeNegotiator.
 *
 * @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
 *   The access checker for themes.
 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
 *   The class resolver.
 * @param string[] $negotiators
 *   An array of negotiator IDs.
 */
public function __construct(ThemeAccessCheck $theme_access, ClassResolverInterface $class_resolver, array $negotiators) {
  ...
}
```

In the previous example, the `ThemeNegotiator` service constructor receives all the defined arguments (`@access_check.theme` and `@class_resolver`) and an array of service ids for all services tagged with `theme_negotiator`.
A service collector like this usually requires the use of a **service locator** to lazily load the actual services when needed:

```php
public function determineActiveTheme(RouteMatchInterface $route_match) {
  foreach ($this->negotiators as $negotiator_id) {
    $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
  }
}
```

This approach optimizes performance by avoiding the instantiation of all tagged services upfront.

> As we saw in [Part 2](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-2/#autowiring-in-controllers-and-hooks), a service locator is a kind of an anti-pattern and should be used sparingly. We'll see in the next section how this has been improved in modern Drupal versions.

## AutowireIterator

Since Symfony 7.0 (and hence Drupal 11.0), you can also use the `AutowireIterator` attribute to inject all services tagged with a specific tag, like in this example:

```php
class CachedDiscoveryClearer implements CachedDiscoveryClearerInterface {

  /**
   * Constructs the CachedDiscoveryClearer service.
   *
   * @param \Traversable $cachedDiscoveries
   *   The cached discoveries.
   */
  public function __construct(
    #[AutowireIterator(tag: 'plugin_manager_cache_clear')]
    protected \Traversable $cachedDiscoveries,
  ) {}
}
```

The `AutowireIterator` attribute allows you to specify a tag name, and the service container will automatically inject all services tagged with that name into the constructor as a `\Traversable` object.

Using `AutowireIterator` fixes both the performance issues of `service_collector` and the anti-pattern of `service_id_collector`, as services are only instantiated when iterated over.

> Newer versions of Drupal are gradually implementing all the features of Symfony's service container, reducing the need for Drupal-specific solutions.

All the features available in Symfony can be used on Drupal as well, like the ability to exclude specific services with the `exclude` parameter, or to set the priority of a service directly on the service class, with the `AsTaggedItem` attribute:

```php
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

#[AsTaggedItem(index: 'handler_one', priority: 10)]
class One {
    // ...
}
```

Luckily, all the documentation explained in the [Symfony documentation](https://symfony.com/doc/7.4/service_container/tags.html#reference-tagged-services) applies to Drupal as well.

> **Note:** `AutowireIterator` and `AsTaggedItem` attributes are only available in modules that declare `autowiring: true` in their `module_name.services.yml` file.

## Conclusion

Service collectors and AutowireIterators are powerful tools in Drupal's service container that help manage and organize related services efficiently. By leveraging these features, developers can create more modular and maintainable code, while also optimizing performance.

Stay tuned for the next parts of this series, where we will continue to explore more advanced features of the Drupal service container:

* **[Part 1](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-1/)** covers service tags, compiler passes, service providers, and autoconfiguration, laying the groundwork for understanding how services are defined and managed within the container.
* **[Part 2](https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-2/)** delves into aliases, autowiring, and name arguments, demonstrating how to resolve and inject dependencies based on type hints, reducing boilerplate code and enhancing maintainability.
* **Part 4** will explore factories, which provide a mechanism for creating services with complex initialization logic.
* **Part 5** will discuss backend overrides, enabling developers to customize service implementations for specific environments or use cases.
* **Part 6** will examine advanced features of the Drupal service container, such as service decoration and lazy loading.

---

This is the Markdown version of an Engineering page of SparkFabrik, for AI agents and crawlers. The full page is at https://www.sparkfabrik.com/en/engineering/drupal-service-container-deep-dive-part-3/.
