Skip to main content
Start of main content.

Picking a Drupal service at runtime with Symfony tagged locators

Picking a Drupal service at runtime with Symfony tagged locators

Or in other words - collecting and accessing services from the container, without injecting the whole container.

Sometimes you need to collect a group of services and pick the right one at runtime, based on some condition. 

Drupal's render pipeline is a good example. It needs a different renderer depending on how a request wants its response back: HTML requests get HtmlRenderer, Ajax requests get AjaxRenderer, a modal dialog gets ModalRenderer. There are many of these in core, all implementing the same interface.

In the past, you might have handled this by injecting the whole service container. Then you'd pull out the service you needed by name, something like \Drupal::service('main_content_renderer.' . $format). While this would work, it would tie your class to the container. This would make it harder to test, and also wouldn't make it obvious which services your code expects to exist. Or, in other cases, you might have a compiler pass collect tagged services and inject the service IDs and the container. LazyContextRepository in core is an example of this pattern.

Since #3414208, Drupal can make use of a neat feature from Symfony instead. It's called a tagged locator. A locator builds the service only when you ask for it. The constructor lists exactly what it can provide, rather than the whole container.

A concrete example already in core is MainContentViewSubscriber, which picks the renderer for the current main content view.

public function __construct(
  protected RouteMatchInterface $routeMatch,
  #[AutowireLocator('render.main_content_renderer', 'format')]
  protected ServiceLocator $renderers,
) {}

The #[AutowireLocator] attribute tells Symfony to collect every service tagged render.main_content_renderer. It indexes them by each one's format attribute. 

main_content_renderer.html:
  class: Drupal\Core\Render\MainContent\HtmlRenderer
  tags:
    - { name: render.main_content_renderer, format: html }
main_content_renderer.ajax:
  class: Drupal\Core\Render\MainContent\AjaxRenderer
  tags:
    - { name: render.main_content_renderer, format: drupal_ajax }

Getting the right renderer is then a single line.

$renderer = $this->renderers->get($wrapper);

E.g. in this example $wrapper corresponds to the 'format' attribute in the tags. e.g. for HtmlRenderer, it has { format: html } for its service entry. If the $wrapper is html, $this->renderes->get($wrapper) would get the HtmlRenderer.

get() only builds the service you ask for. HtmlRenderer doesn't get instantiated to serve an Ajax request. MainContentViewSubscriber doesn't need to know which renderer services exist, just the tag they share.

Wiring one up without auto-wiring

#[AutowireLocator] is the simplest way to set this up. If your class is already using auto-wiring, add this attribute to the constructor - like the example above.

If your service isn't using auto-wiring, you can still achieve it via the !tagged_locator argument type.

export_manager:
  class: Drupal\Example\ExportManager
  arguments: ['%export.config%', !tagged_locator { tag: 'export.format_handler', index_by: 'format' }]

This builds the same locator, but as an explicit constructor argument. 

Available since 10.3

!tagged_locator and !tagged_iterator in services.yml have been supported in core since Drupal 10.3.0 or 11.0.0 and later. My colleague Kim Pepper filed the issue and worked on the change with longwave and alexpott, and it landed back in 2024.

Whilst the issue was titled "Add support for tagged_iterator to YamlFileLoader", and the discussion is entirely about tagged_iterator the patch ported Symfony's resolveServices() method as a whole. That method handles tagged, tagged_iterator and tagged_locator in the same block of code, so tagged_locator support landed as part of the same change.

Wrapping up

If you're injecting one of several services that implement an interface, and picking between them based on some condition, this pattern is worth knowing. Tag each implementation, index by whatever distinguishes them, and let the locator do the lookup.

Related Articles

An abstract image of the UX process

How we built protected search in Drupal using OpenSearch

Recently, we worked on a project for Catholic Schools NSW, where one of the key requirements was controlling access to content. Some pages and resources needed to be available only to authenticated users, while the rest of the site remained publicly accessible. 

That requirement didn’t stop at the website itself, but it also extended to search. Authenticated users needed to see protected content in their results, while anonymous visitors should only see public content. 

This presented an interesting challenge. In a decoupled architecture, search doesn’t automatically inherit Drupal’s access controls, so we needed a way to make search permission-aware without compromising performance. We used OpenSearch to build a solution that ensured users only saw content they were authorised to access, while keeping the search experience seamless.