Tagged Service Collectors
When to Use
When you need to build an extensible system where multiple services can register themselves to be used by a manager service — plugin-like architecture without the plugin API overhead.
Steps
-
Define the collector service
services: my_module.handler_manager: class: Drupal\my_module\HandlerManager tags: - { name: service_collector, tag: my_handler, call: addHandler } -
Implement the collector class with addHandler() method
namespace Drupal\my_module; class HandlerManager { protected array $handlers = []; public function addHandler(HandlerInterface $handler, int $priority = 0) { $this->handlers[$priority][] = $handler; } public function getHandlers(): array { krsort($this->handlers); return array_merge(...$this->handlers); } } -
Define handler services with the tag
services: my_module.handler_foo: class: Drupal\my_module\Handler\FooHandler tags: - { name: my_handler, priority: 100 } my_module.handler_bar: class: Drupal\my_module\Handler\BarHandler tags: - { name: my_handler, priority: 50 } -
Alternative: Use !tagged_iterator (Drupal 11+ / Symfony 7+)
services: my_module.handler_manager: class: Drupal\my_module\HandlerManager arguments: - !tagged_iterator my_handler
namespace Drupal\my_module;
class HandlerManager {
public function __construct(
protected iterable $handlers,
) {}
public function getHandlers(): array {
return iterator_to_array($this->handlers);
}
}
Decision Points
| At this step... | If... | Then... |
|---|---|---|
| Choosing collection method | Drupal 10.x | Use service_collector tag |
| Choosing collection method | Drupal 11+ | Prefer !tagged_iterator for lazy loading |
| Naming the tag | Tag is public API | Document tag name and required interface |
| Naming the tag | Tag is internal | Use module-prefixed name |
| Handling priority | Order matters | Accept $priority param in addHandler(), sort in getter |
Pattern
Named Arguments in addHandler() (Drupal 11+):
// Compiler pass injects tag attributes as named parameters
public function addHandler(
HandlerInterface $handler,
string $id, // Service ID
int $priority = 0,
string $custom_attribute = '',
) {
$this->handlers[$priority][$id] = [
'handler' => $handler,
'custom' => $custom_attribute,
];
}
services:
my_module.handler_foo:
tags:
- { name: my_handler, priority: 100, custom_attribute: 'foo_value' }
Reference: /core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php, Service collectors deep dive
Common Mistakes
- Eagerly instantiating all handlers —
service_collectorcreates all instances at manager creation; use!tagged_iteratorfor lazy loading - Not sorting by priority — Handlers are passed in random order; sort them yourself if order matters
- Missing type hint on $handler parameter — TaggedHandlersPass requires type hint to validate handlers
- Using service locator pattern — Don't inject the container to fetch handlers; collect them via tags
- Forgetting to document the tag — Other developers won't know to use your tag without documentation