Change record status: 
Introduced in branch: 
Introduced in version: 

Service collector: what is it?

It is currently possible to tag a service as a service_collector. It can then specify a service tag to find which services to collect —
breadcrumb_builder in this example:

{ name: service_collector, tag: breadcrumb_builder, call: addBuilder }

Then each service that needs to be collected just needs that corresponding tag:

{ name: breadcrumb_builder, priority: 100 }

This will then call the addBuilder() method on the service tagged as a service_collector, thus adding each collected service.

Problem: service collectors are expensive

However, this is not ideal in a lot of situations as it requires all services to be instantiated, to satisfy the addBuilder() method calls on the service collector.

Solution: Service ID collector

A service_id_collector tag has been added. This still collects services based on tags and sorts them by priority as before, but instead of performing method calls on the collecting service for each collected service, it just adds an array of sorted service IDs as a constructor parameter to the collecting service.

So similar to regular service collectors, add a service_id_collector tag to the collecting service:

{ name: service_id_collector, tag: theme_negotiator }

Services you want to be collected can be tagged the same as before (see above).

Your collecting service now just needs to account for an additional constructor parameter that is appended automatically by the TaggedHandlersPass container compiler pass: an array containing the sorted, collected service IDs.

Service ID collector: how to use

Tag it as demonstrated above, and expect an additional parameter to be passed into the service's constructor automatically.

However, you'll also want to inject the @class_resolver service so that you can actually lazily instantiate the collected service IDs. For example, when changing from a "regular" service collector to a service ID collector, this is what the changes would look like:

     class: Drupal\Core\Theme\ThemeNegotiator
-    arguments: ['@access_check.theme']
+    arguments: ['@access_check.theme', '@class_resolver']
-      - { name: service_collector, tag: theme_negotiator, call: addNegotiator }
+      - { name: service_id_collector, tag: theme_negotiator }

And then the corresponding collecting service would look like this:

// As an example, let's assume we have a service that is negotiating something.
class MyNegotiationService {

  public function __construct($whatever, $params, $you, $have, $already, ClassResolverInterface $class_resolver, array $negotiator_service_ids) {
    $this->classResolver = $class_resolver;
    $this->negotiatorServiceIds = $negotiator_service_ids;

  public function doThing() {
    foreach ($this->negotiatorServiceIds as $negotiator_service_id) {
      $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_service_id);

      if ($negotiator->applies()) {
        // DO THINGS!


Which service collectors were updated to be service ID collectors in Drupal core?

  1. theme.negotiator (\Drupal\Core\Theme\ThemeNegotiator)
  2. route_filter.lazy_collector (Drupal\Core\Routing\LazyRouteFilter) — once #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent lands.
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other updates done