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: truein the module’s.services.ymlfile.
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.


