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.

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 EntityTypeManagerInterface $entityTypeManager, and goes: "Oh, I know that class! I'll inject the right service automatically."
Magic? Almost. But it’s actually very elegant engineering 🌈

👨‍🍳 Au menu

1What DI used to look like in Drupal (and why it was painful)
2How FQCN aliases made Autowiring possible
3How to use Autowiring in Services, Controllers, Forms, Blocks and Hooks
4What 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 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.
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 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.

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:
  1. The service is registered using its FQCN as the service name (which also serves as the alias)
  2. 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.2ControllerBase 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 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 #[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 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:
    The 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.
      Less boilerplate. Fewer opportunities to introduce bugs (wrong order in 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
      Drupal.org (June 2021) — Services can be autowired.
      https://www.drupal.org/node/3218156
      Drupal.org (June 2023) — Autowiring is enabled in core.services.yml.
      https://www.drupal.org/node/3366757
      Drupal.org (Oct 2024) — Support for object oriented hook implementations using autowired services.
      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
      Kevin Wenger (Sept 2025)  —Drupal Object-Oriented Hooks.
      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
      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/
      Brian Walters (Oct 2018) — Symfony 4 Autowiring in a Nutshell.
      https://medium.com/@BDubCodes/symfony-4-autowiring-in-a-nutshell-23a80b15ec80