Autowiring in Drupal – the hidden gem many dev still ignore
Auteur(s) de l'article
⚠️ Heads up — This article is about Dependency Injection in Drupal. If you’re not yet comfortable with DI, services or the service container, some sections might feel a bit abstract. I’d recommend reading this before article.
It’s a random Tuesday, I’m reviewing a pull request and I see the classic DI boilerplate. You know the one.
public static function create(ContainerInterface $container): static {
return new static(
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('logger.factory'),
$container->get('config.factory')
);
}I stare at the screen. I sip my coffee. I think:
“ We’ve been doing this since Drupal 8. It’s 2024. Symfony has Autowiring since so many years — can’t we have a better way to acheive DI in Drupal ? ”
There is. And it’s been available since Drupal 10.2.
Let me introduce you to Autowiring for Drupal.
Let me introduce you to Autowiring for Drupal.
What is Autowiring?
In a nutshell, Autowiring is a feature of the Service Container (from Symfony) that allows automatic dependency injection based on Type Hints.
Instead of you manually telling the container “hey, inject this service, then that one, then that other one”, the container figures it out by looking at the type hints in your constructor.
It reads your code, sees
It reads your code, sees
EntityTypeManagerInterface $entityTypeManager, and goes: "Oh, I know that class! I'll inject the right service automatically."
👨🍳 Au menu
| 1 | What DI used to look like in Drupal (and why it was painful) |
|---|---|
| 2 | How FQCN aliases made Autowiring possible |
| 3 | How to use Autowiring in Services, Controllers, Forms, Blocks and Hooks |
| 4 | What to do when Autowiring fails |
Back in the Day — The Old-School DI in Drupal
Before Drupal 10.2, injecting services was a ritual. A repetitive, verbose, copy-paste ritual. Depending on what you were building, you had two flavors of pain.
Flavor 1: The services.yml way
Every service had a unique name, and every dependency had to be declared explicitly in your module’s
services.yml:# my_module.services.yml
services:
my_module.my_service:
class: Drupal\my_module\MyService
arguments:
- '@entity_type.manager'
- '@current_user'
- '@logger.factory'
- '@config.factory'And then your class would receive those services in the constructor — in the exact same order as declared in the YAML.
Miss one? Wrong order? Enjoy your
Miss one? Wrong order? Enjoy your
ArgumentCountError at runtime 🎉.namespace Drupal\my_module;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Session\AccountInterface;
class MyService {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly AccountInterface $currentUser,
private readonly LoggerChannelFactoryInterface $loggerFactory,
private readonly ConfigFactoryInterface $configFactory,
) {}
}Functional? Yes.
Maintainable? Questionable.
Maintainable? Questionable.
Every time you add a dependency, you update the YAML, the constructor and pray you didn’t swap the order.
Flavor 2: The create() method
For Controllers, Forms and Blocks (and many others classes that implement
ContainerInjectionInterface without using a declaration inservices.yml) Drupal used - and still uses by default - the static create() method:namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyController extends ControllerBase {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly AccountInterface $currentUser,
) {}
public static function create(ContainerInterface $container): static {
return new static(
$container->get('entity_type.manager'),
$container->get('current_user'),
);
}
}This approach works, but it's essentially a service locator anti-pattern.
You're manually pulling services out of the container rather than letting the container inject them. Every new dependency means updating both the constructor and the
You're manually pulling services out of the container rather than letting the container inject them. Every new dependency means updating both the constructor and the
create() method.“ Two places. Every. Single. Time. ”
FQCN Aliases
Before Autowiring could exist, Drupal needed to lay some groundwork.
Starting from Drupal 10.1, most core services were aliased using their Fully Qualified Class Name (FQCN). What does that mean?
Previously, a service was only reachable via its unique string identifier, like
webprofiler.profiler. You could never refer to it by its class name.# Before: only reachable via string identifier
webprofiler.profiler:
class: Drupal\webprofiler\Profiler\Profiler
arguments:
- '@webprofiler.file_storage'
- '@logger.channel.webprofiler'
- '@config.factory'With FQCN aliases, the same service becomes also accessible via its class name:
# After: also reachable via the FQCN
webprofiler.profiler:
class: Drupal\webprofiler\Profiler\Profiler
arguments:
- '@webprofiler.file_storage'
- '@logger.channel.webprofiler'
- '@config.factory'
# This alias makes autowiring possible ✨
Drupal\webprofiler\Profiler\Profiler: '@webprofiler.profiler'When a service name equals the FQCN of the service class, the container can automatically resolve and inject dependencies simply by matching the type hint.
That's Autowiring.
That's Autowiring.
Autowiring in Services
With Autowiring, that verbose
services.yml we saw earlier becomes almost whisper-quiet:Old way
services:
webprofiler.profiler_listener:
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'With Autowiring
services:
_defaults:
autowire: true # ✨ Enable autowiring for all services in this file
# The FQCN is both the service name and the alias
Drupal\webprofiler\EventListener\ProfilerListener: ~This works because:
- The service is registered using its FQCN as the service name (which also serves as the alias)
- The container reads the constructor type hints and injects the matching services automatically
Your class doesn’t change at all, the constructor type hints do all the talking:
namespace Drupal\webprofiler\EventListener;
use Drupal\webprofiler\Matcher\ExcludePathRequestMatcher;
use Drupal\webprofiler\Profiler\Profiler;
use Symfony\Component\HttpFoundation\RequestStack;
class ProfilerListener {
public function __construct(
// The container resolves these automatically via their FQCN aliases ✨
private readonly Profiler $profiler,
private readonly RequestStack $requestStack,
private readonly ExcludePathRequestMatcher $excludePathMatcher,
) {}
}Autowiring in Controllers
Starting from Drupal 10.2,
ControllerBase uses the AutowireTrait. This means you can finally delete that create() method and let the container do its job.Old way
class DashboardController extends ControllerBase {
public function __construct(
private readonly Profiler $profiler,
private readonly TemplateManager $templateManager,
) {}
// Goodbye, old friend. We won't miss you. 👋
public static function create(ContainerInterface $container): static {
return new static(
$container->get('webprofiler.profiler'),
$container->get('webprofiler.template_manager'),
);
}
}With Autowiring (Drupal 10.2+)
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\AutowireTrait;
class DashboardController extends ControllerBase {
// ✨ That's it. No more create() method.
use AutowireTrait;
public function __construct(
private readonly Profiler $profiler,
private readonly TemplateManager $templateManager,
) {}
// No create() method needed anymore. 🎉
}The
AutowireTrait can be used on any class that implements ContainerInjectionInterface.Autowiring in Forms
FormBase does not yet use AutowireTrait by default (yes, it's 2026, I know 😅), but you can manually add the trait to your custom forms.namespace Drupal\my_module\Form;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class MyForm extends FormBase {
// ✨ Just add this and you're done.
use AutowireTrait;
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
}Autowiring in Blocks
Blocks (which implement
The pattern is the same: add the trait, remove the
ContainerFactoryPluginInterface) also benefit from AutowireTrait.The pattern is the same: add the trait, remove the
create() method:namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[Block(
id: 'my_module_my_block',
admin_label: new TranslatableMarkup('My Block'),
)]
class MyBlock extends BlockBase implements ContainerFactoryPluginInterface {
// ✨ Replaces the create() method.
use AutowireTrait;
public function __construct(
array $configuration,
string $plugin_id,
mixed $plugin_definition,
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
}Autowiring in Hooks
Starting from Drupal 11.1, hooks can be defined inside classes using the
And the best part? All hook classes are automatically registered as autowired services. You don't need to configure anything in
#[Hook] attribute.And the best part? All hook classes are automatically registered as autowired services. You don't need to configure anything in
services.yml.namespace Drupal\file\Hook;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\field\Entity\FieldStorageConfig as FieldStorageConfigInterface;
use Drupal\views\EntityViewsDataProvider as FieldViewsDataProvider;
use Drupal\Core\Hook\Attribute\Hook;
class FileViewsHooks {
// All dependencies are autowired automatically. 💫
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 {}
}No
No manual wiring. Just clean, readable PHP.
services.yml. No create().No manual wiring. Just clean, readable PHP.
I wrote a dedicated article about Object-Oriented Hooks in Drupal if you want to go deeper.
When Autowiring fails
Not all services can be autowired automatically. You might have seen this terrifying screen:

AutowiringFailedException“ Why does this happen? Because some interfaces have multiple implementations or they don’t use FQCN alias. LoggerInterface is the perfect example: Drupal has dozens of logger channels (logger.channel.php, logger.channel.drupal, logger.channel.your_module…). The container doesn't know which one you want, so instead of guessing, it throws an exception. ”
In this case, you can use the
#[Autowire] attribute to explicitly specify which service to inject:namespace Drupal\my_module;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MyService {
public function __construct(
// ✨ Tell the container exactly which logger channel to inject.
#[Autowire(service: 'logger.channel.my_module')]
private readonly LoggerInterface $logger,
) {}
}Think of
#[Autowire] as leaving a post-it note for the container: "Yes I know there are many LoggerInterface implementations , use THIS specific one."Other cases where you might need
#[Autowire]:- Tagged services — when you want a collection of services sharing a tag, injected as an iterable.
- Services without a FQCN alias — when an older or contributed service is still registered only under its legacy string identifier and hasn't been aliased yet.
Conclusion
Autowiring isn’t a revolutionary new concept, Symfony has had it for years. But it finally landed properly in Drupal with 10.2 and continue to be implemented in Drupal 11+.
It will genuinely changes how you write code day to day.
It will genuinely changes how you write code day to day.
Less boilerplate. Fewer opportunities to introduce bugs (wrong order in
Your constructor becomes the single source of truth for your dependencies the way it always should have been.
create()? wrong service name?). More readable, maintainable code.Your constructor becomes the single source of truth for your dependencies the way it always should have been.
Yes, it arrived quite a long time ago on Drupal, drop by drop. But it’s here now, it’s used by the Core and there’s no good reason not to use it on your next project.
If you’re maintaining older Drupal modules, this is also a great refactoring opportunity, removing
create() methods one by one is surprisingly satisfying. Trust me.❤️ Love on your keyboards
Sources
Drupal.org (Oct 2023) — Controllers can be autowired and a create() method is no longer always necessary.
https://www.drupal.org/node/3395716
https://www.drupal.org/node/3395716
Drupal.org (June 2023) — Autowiring is enabled in
https://www.drupal.org/node/3366757
core.services.yml.https://www.drupal.org/node/3366757
Drupal.org (Feb 2026) — Dependency Injection in a form.
https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/dependency-injection-in-a-form
https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/dependency-injection-in-a-form
Drupal.org (Oct 2024) — Support for object oriented hook implementations using autowired services.
https://www.drupal.org/node/3442349
https://www.drupal.org/node/3442349
Drupal.org (Jan 2026) — Add create() factory method with autowired parameters to PluginBase.
https://www.drupal.org/project/drupal/issues/3452852
https://www.drupal.org/project/drupal/issues/3452852
Kevin Wenger (Sept 2025) —Drupal Object-Oriented Hooks.
https://wengerk.medium.com/drupal-object-oriented-hooks-989caf751e5b
https://wengerk.medium.com/drupal-object-oriented-hooks-989caf751e5b
Jane (May 2023) — Can services be autowired?.
https://drupal.stackexchange.com/questions/315397/can-services-be-autowired
https://drupal.stackexchange.com/questions/315397/can-services-be-autowired
Luca Lusso (Dec 2025) — Drupal service container deep dive (Part 2): aliases, autowiring, and named arguments.
https://tech.sparkfabrik.com/en/blog/drupal-service-container-deep-dive-part-2/
https://tech.sparkfabrik.com/en/blog/drupal-service-container-deep-dive-part-2/
Brian Walters (Oct 2018) — Symfony 4 Autowiring in a Nutshell.
https://medium.com/@BDubCodes/symfony-4-autowiring-in-a-nutshell-23a80b15ec80
https://medium.com/@BDubCodes/symfony-4-autowiring-in-a-nutshell-23a80b15ec80