Drupal service container deep dive (Part 2): aliases, autowiring, and named arguments
This article is the second part of a series exploring the Drupal service container, focusing on aliases, autowiring, and named arguments.
Disclaimer: Our Tech Blog is a showcase of the amazing talent and personal interests of our team. While the content is reviewed for accuracy, each article is a personal expression of their interests and reflects their unique style. Welcome to our playground!
In the first part of this series, we explored the basics of the Drupal service container, including how services are defined and how dependencies are injected. We discussed concepts such as tags, compiler passes, service providers, and autoconfiguration. In this second part, we will delve deeper into more advanced concepts such as aliases, autowiring, and named arguments.
Aliases
An alias in the Drupal service container is a way to create an alternative name for an existing service. This is particularly useful when you want to refer to a service by a different name without changing the original service definition.
request_stack:
class: Symfony\Component\HttpFoundation\RequestStack
tags:
- { name: persist }
Symfony\Component\HttpFoundation\RequestStack: '@request_stack'
In this example, we define a service request_stack and then create an alias Symfony\Component\HttpFoundation\RequestStack that points to the same service. This allows us to refer to the RequestStack service using either name interchangeably. This specific example works because a service name is just a string, and we can use anything that is a valid string as a service name, including class names.
A service name is just a string:
config.factory,entity_type.manager,Symfony\Component\HttpFoundation\RequestStack, are all valid service names.
Starting from Drupal 10.1, most of the core services have been aliased with the Fully Qualified Class Name (FQCN) of the service class.
Change record: https://www.drupal.org/node/3323122
Of course, you can create your own aliases in your custom modules to improve code readability and maintainability:
webprofiler.profiler:
class: Drupal\webprofiler\Profiler\Profiler
arguments:
[
'@webprofiler.file_storage',
'@logger.channel.webprofiler',
'@config.factory',
]
Drupal\webprofiler\Profiler\Profiler': '@webprofiler.profiler'
When a service name is equal to the service class FQCN, the service container can automatically resolve and inject dependencies simply by matching the requested FQCN.
This feature is called autowiring.
Autowiring
Autowiring is a feature of the Drupal (and Symfony) service container that allows for automatic dependency injection based on type hints. When a service is defined with autowiring enabled, the container will automatically resolve and inject the required dependencies based on the type hints specified in the constructor or method signatures.
The service will be instantiated in exactly the same way as before, but there is no need to explicitly specify which arguments are required; the interfaces/classes that are declared in the service constructor will be used to discover which services should be injected.
Autowire can be enabled at service level, like this:
services:
_defaults:
autoconfigure: true
webprofiler.profiler_listener:
class: Drupal\webprofiler\EventListener\ProfilerListener
autowire: true
or globally for all services, like this:
services:
_defaults:
autoconfigure: true
autowire: true
webprofiler.profiler_listener:
class: Drupal\webprofiler\EventListener\ProfilerListener
In Symfony, when a single service exists for a given interface or class, the service container automatically creates an alias for it using the FQCN. This means that you can type-hint against the interface or class in your constructor, and the container will automatically inject the correct service. This is not the case in Drupal, where you need to explicitly create the alias as shown above.
Multiple services for the same interface
Usually, a Drupal module extends the logger.channel_base service to build a new logger with the module name, like in this example:
services:
_defaults:
autowire: true
autoconfigure: true
logger.channel.module_name:
parent: logger.channel_base
arguments: ['module_name']
my_service:
class: Drupal\module_name\MyService
Both logger.channel_base, logger.channel.module_name, and every logger defined by Core and modules are instances of the same interface: Psr\Log\LoggerInterface. As there are many services in the service container that implement Psr\Log\LoggerInterface, which specific implementation will be injected in a constructor like this?
use Psr\Log\LoggerInterface;
class MyService {
public function __construct(
private readonly LoggerInterface $logger,
) {
}
}
In this case, the service container will throw an exception because it cannot determine which specific service to inject:
Some services cannot be autowired, usually because the interface is implemented by multiple different services (like loggers or cache bins in Drupal).
In this case, we must use the #[Autowire] attribute to clearly specify which service to inject, like this:
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MyService {
public function __construct(
#[Autowire(service: 'logger.channel.module_name')]
private readonly LoggerInterface $logger,
) {
}
}
Autowiring in Controllers and Hooks
A controller is not a service and it’s not loaded by the service container. When a route is matched, Drupal uses the controller loader service (controller_resolver) to create an instance of the controller class. The controller loader calls the create method on the controller class to instantiate it. The create method receives the entire service container and creates a new instance of the controller class with the required services. This is an implementation of the service locator anti-pattern, as shown in:
class DashboardController extends ControllerBase {
final public function __construct(
private readonly Profiler $profiler,
private readonly TemplateManager $templateManager,
) {
}
public static function create(ContainerInterface $container): DashboardController {
return new static(
$container->get('webprofiler.profiler'),
$container->get('webprofiler.template_manager'),
);
}
}
Starting from Drupal 10.2, ControllerBase (the base class for controllers in Drupal) uses the AutowireTrait, so if the controller constructor uses type hints and the #[Autowire] attribute where necessary, we can take advantage of autowiring in controllers as well.
AutowireTraituses reflection to read the constructor parameters and their attributes, so it has a performance impact.
use Drupal\Core\DependencyInjection\AutowireTrait;
class DashboardController extends ControllerBase {
use AutowireTrait;
final public function __construct(
private readonly Profiler $profiler,
private readonly TemplateManager $templateManager,
) {
}
}
As we can see in the previous example, the create method is no longer required. The AutowireTrait can be used for any classes that implement ContainerInjectionInterface (for some reason, FormBase does not use it yet, but you can easily add it to your custom forms).
With the upcoming Drupal 11.3, a similar feature will be available for plugins too: Add create() factory method with autowired parameters to PluginBase.
Starting from Drupal 11.1, hooks can be defined in classes (https://www.drupal.org/node/3442349). All hook classes are automatically registered as autowired services, so you can use autowiring in hook classes as well:
namespace Drupal\file\Hook;
class FileViewsHooks {
public function __construct(
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly EntityFieldManagerInterface $entityFieldManager,
protected readonly ?FieldViewsDataProvider $fieldViewsDataProvider,
) {}
#[Hook('field_views_data')]
public function fieldViewsData(FieldStorageConfigInterface $field_storage): array {
$data = $this->fieldViewsDataProvider->defaultFieldImplementation($field_storage);
}
}
Autowiring in Services vs Non-Services
It’s important to note that while services benefit from autowiring when properly configured, there are cases where you might want to use autowiring but cannot modify the class constructor to fix the type hint or to add the #[Autowire] attribute. This is where named arguments come into play.
Named Arguments
Named arguments provide an alternative way to specify dependencies in the service definition file itself, without requiring changes to the PHP code. This is particularly useful when:
- You’re working with third-party classes that you cannot modify
- You want to keep dependency configuration separate from the code
- You need to inject a specific service when multiple implementations of the same interface exist (as an alternative to using the
#[Autowire]attribute)
Named arguments allow us to specify which arguments to pass to a service when it is being instantiated, by name rather than by position. The syntax uses the parameter name (with a $ prefix) as the key in the arguments section.
For example, let’s say we have a service class with a constructor like this:
namespace Symfony\Component\Messenger\Middleware;
class SendMessageMiddleware {
public function __construct(
SendersLocatorInterface $sendersLocator,
EventDispatcherInterface $eventDispatcher = null,
) {
}
}
Instead of passing arguments by position, we can use named arguments in the service definition:
messenger.middleware.send_message:
class: Symfony\Component\Messenger\Middleware\SendMessageMiddleware
arguments:
$eventDispatcher: '@event_dispatcher'
In this example, we’re explicitly specifying that the $eventDispatcher parameter should receive the @event_dispatcher service. This is much clearer than using positional arguments, especially when dealing with optional parameters or when you want to skip some arguments.
Named arguments are particularly powerful when combined with autowiring. The container will:
- First try to autowire all type-hinted parameters based on their types
- Then override specific parameters with any named arguments you’ve defined
- Use default values for any remaining optional parameters
This means you can let autowiring handle most dependencies automatically while using named arguments only for the specific cases where you need explicit control, such as when multiple services implement the same interface:
services:
_defaults:
autowire: true
autoconfigure: true
my_custom_service:
class: Drupal\my_module\MyCustomService
arguments:
$logger: '@logger.channel.my_module'
In this example, most dependencies of MyCustomService will be autowired, but we’re explicitly specifying which logger to inject using a named argument, solving the ambiguity problem we discussed earlier.
Bringing It All Together
Now that we understand aliases, autowiring, and named arguments, let’s see how they work together to simplify service definitions.
Traditional Service Definition
In older Drupal code, a service definition might look like this:
services:
webprofiler.profiler_listener:
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
This approach requires:
- Explicitly listing every dependency
- Maintaining the correct order of arguments
- Updating the YAML file whenever constructor parameters change
Modern Service Definition with Aliases and Autowiring
With the concepts we’ve covered, we can dramatically simplify this:
services:
_defaults:
autoconfigure: true
autowire: true
Drupal\webprofiler\EventListener\ProfilerListener: ~
This works because:
- Aliases: The service is registered using its FQCN, which serves as both the service name and alias
- Autowiring: The container automatically resolves dependencies based on type hints in the constructor
- Autoconfiguration: The service is automatically tagged if it implements certain interfaces
In Yaml,
~symbol (tilde) is equivalent to an empty array or null, meaning no additional configuration is needed. In this case, the tilde can be omitted entirely, but in my opinion, it’s clearer to explicitly show that no further configuration is necessary.
Conclusion
In this second part of our deep dive into the Drupal service container, we have explored three powerful concepts that work together to modernize dependency injection in Drupal:
- Aliases enable us to reference services by their class names, making code more intuitive and refactoring safer
- Autowiring eliminates boilerplate by automatically resolving dependencies based on type hints
- Named arguments provide surgical precision when we need explicit control over specific dependencies
Together, these features reduce boilerplate code, improve maintainability, and make service definitions more readable. They represent a significant evolution in how Drupal developers work with the service container.
Stay tuned for the next parts of this series, where we will continue to explore more advanced features of the Drupal service container:
- Part 1 covers service tags, compiler passes, service providers, and autoconfiguration, laying the groundwork for understanding how services are defined and managed within the container.
- Part 3 will cover service collectors, which aggregate multiple services into a single service for streamlined access.
- Part 4 will explore factories, which provide a mechanism for creating services with complex initialization logic.
- Part 5 will discuss backend overrides, enabling developers to customize service implementations for specific environments or use cases.
- Part 6 will examine advanced features of the Drupal service container, such as service decoration and lazy loading.