Lee RowlandsLead Developer

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.
#[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.
!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.
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.

VIDEO: In his DrupalCon Barcelona session, Mohit explored how Symfony Console commands can improve your Drupal workflow, offering enhanced flexibility and efficiency.

We recently shipped semantic search on GovCMS for Cancer Australia, the national agency for cancer control. It's backed by OpenSearch and AWS Bedrock, with AI-generated answers layered on top of the results. Here's how we built it, and how it's hosted.

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.