Autowiring all the things! - DrupalCon Vienna 2025 - Luca Lusso, SparkFabrik

sparkfabrik 0 views 52 slides Oct 15, 2025
Slide 1
Slide 1 of 52
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52

About This Presentation

Have you ever wondered how Drupal efficiently manages the complex dependencies that power its modules? The service container is the backbone of modern Drupal applications, streamlining service management and boosting performance. With Drupal 11, it’s more potent than ever, fully embracing Symfony�...


Slide Content

Autowiring all the things!
lussoluca

3
DrupalCon Vienna 2025Autowiring all the things!
Drupal / PHP / Go developer @ SparkFabrik
Drupal contributor (WebProfiler, Monolog, Symfony
Messenger, Search API Typesense), AI maker and speaker

Drupal.org: https://www.drupal.org/u/lussoluca
LinkedIn: www.linkedin.com/in/lussoluca
Slack (drupal.slack.com): lussoluca
Mastodon: @[email protected]
Luca Lusso
lussoluca
3

WE ARE A TECH COMPANY
OF ENGINEERS, DEVELOPERS AND DESIGNERS,
WHO WILL THINK, DESIGN
AND BUILD YOUR CUSTOM APPLICATIONS,
MODERNIZE YOUR LEGACY
AND TAKE YOU
TO THE CLOUD NATIVE ERA
44
Autowiring all the things! DrupalCon Vienna 2025

5
PROUD
OF OUR
PARTNERSHIPS
Autowiring all the things! DrupalCon Vienna 2025
We help italian businesses to
bridge the gap with China
thanks to our official
partnership with Alibaba Cloud
We are Google Cloud Platform
Technology Partner
We are official AWS partners

MEMBERSHIPS &
COLLABORATIONS
6
DrupalCon Vienna 2025Autowiring all the things!
Silver member of:
Supporter of:

7
Autowiring all the things! DrupalCon Vienna 2025
Services
A service is a highly specific, reusable, and stateless object that
performs a single, well-defined task or provides a particular
functionality.
For example:
●ConfigFactory -> give access to configurations
●EntityTypeManager -> give access to entities
●FormBuilder -> manage forms
●EmailValidator -> validate emails
●Database -> give access to the database
●300+ in Core only…
Where the business logic is

8
Autowiring all the things! DrupalCon Vienna 2025
Service container
Services are not typically created manually using the new keyword
in a class. Instead, they are registered with a central registry called
the service container (also known as an IoC container). The
container is responsible for:
●Service instantiation
●Dependency Resolution
●Configuration and Lifecycle Management
Where the services are

9
Autowiring all the things! DrupalCon Vienna 2025
Service container
The use of services and a service container promotes loose
coupling.
A class that uses a service doesn't need to know how that service is
created or what its internal dependencies are. It simply requests
the service from the container, and the container handles all the
complexities.
This makes it easy to swap out one implementation of a service for
another without changing the code that uses it.
Where the services are

10
Autowiring all the things! DrupalCon Vienna 2025
Service container
10
SOLID
Single Responsibility Principle:
A service should do one and only one task
Interface Segregation Principle:
A service interface should have a limited
set of methods to be implemented
Dependency Inversion Principle:
A service must not depend on other
services concrete implementation; it
must depend only on interfaces

11
Autowiring all the things! DrupalCon Vienna 2025
Modules to look at
Nowadays, both Core and some contributed modules are starting to
use advanced Service Container features like the one we’ll explore
in the next slides.
All the presented examples comes from
[1]
:
●Drupal 11 Core
●WebProfiler
●Symfony Messenger + Drupal: Realtime Queues and Cron

[1]: Some of the code has been changed to simplify the explanation
Where to take inspiration from

12
Autowiring all the things! DrupalCon Vienna 2025
# in web/modules/contrib/webprofiler/webprofiler.services.yml
webprofiler.matcher.exclude_path :
class: Drupal\webprofiler\RequestMatcher\WebprofilerRequestMatcher
arguments: ['@path.matcher', '@config.factory', 'exclude_paths']

# in core/core.services.yml
path.matcher:
class: Drupal\Core\Path\PathMatcher
arguments: ['@config.factory', '@current_route_match']
config.factory:
class: Drupal\Core\Config\ConfigFactory
current_route_match:
class: Drupal\Core\Routing\CurrentRouteMatch
arguments: ['@request_stack']
request_stack:
class: Symfony\Component\HttpFoundation\RequestStack

Services dependencies

13
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\webprofiler\RequestMatcher;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;

class WebprofilerRequestMatcher implements RequestMatcherInterface {

public function __construct(
private readonly PathMatcherInterface $pathMatcher,
private readonly ConfigFactoryInterface $configFactory,
private readonly string $configuration,
) {...}
Services dependencies
When my code asks for the
webprofiler.matcher.exclude_path
service, the Drupal service container first
builds all the required services and finally
builds an instance of the
WebprofilerRequestMatcher class by
injecting all the dependencies in the
__construct method.
webprofiler/src/RequestMatcher/WebprofilerRequestMatcher.php

DrupalCon Vienna 2025Autowiring all the things!
Service
Container
features
14
●Tags
●Compiler passes
●Service providers
●Autoconfiguration
●Aliases
●Autowiring
●Named arguments
●Service visibility
●Abstract services
●Abstract service arguments
●Calls
●Service collectors
●Factories
●Backend overrides

DrupalCon Vienna 2025Autowiring all the things!
Service
Container
features
15
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
tags:
- { name: event_subscriber }
Drupal\webprofiler\EventListener\ProfilerListener : ~
From this:
To this:

16
Autowiring all the things! DrupalCon Vienna 2025

DrupalCon Vienna 2025Autowiring all the things!
Step 1
remove tag definition
17
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
tags:
- { name: event_subscriber }
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
From this:
To this:

18
Autowiring all the things! DrupalCon Vienna 2025
webprofiler.database:
class: Drupal\webprofiler\DataCollector\DatabaseDataCollector
arguments: ['@database', '@config.factory']
tags:
- {
name: data_collector,
template: '@webprofiler/Collector/database.html.twig' ,
id: 'database',
label: 'Database',
priority: 750,
}

Tags
A tag has a mandatory name and an
optional list of attributes.
In the example:
●name : data_collector
●attributes:
○template: @webprofiler/…
○id: database
○label: Database
○priority: 750

To group services together
webprofiler/webprofiler.services.yml

19
Autowiring all the things! DrupalCon Vienna 2025
webprofiler.database:
class: Drupal\webprofiler\DataCollector\DatabaseDataCollector
arguments: ['@database', '@config.factory']
tags:
- {
name: data_collector,
template: '@webprofiler/Collector/database.html.twig' ,
id: 'database',
label: 'Database',
priority: 750,
}

Tags
Tags on their own don't actually alter the
functionality of your services in any way.
We need a compiler pass to find and
process tagged services.
To group services together
webprofiler/webprofiler.services.yml

20
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\webprofiler\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface ;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class ProfilerPass implements CompilerPassInterface {

public function process(ContainerBuilder $c): void {

foreach ($c->findTaggedServiceIds('data_collector', TRUE) as $id => $attributes) {
...
}
}
}

Compiler passes
●The service container is “compiled”,
all its services are collected and
processed to build the final
container
●A compiler pass must be registered
to the service container builder
process. We need a service provider
for that
To process services
webprofiler/src/Compiler/ProfilerPass.php

21
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\webprofiler;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

class WebprofilerServiceProvider extends ServiceProviderBase {

public function register(ContainerBuilder $container): void {
[...]
}

public function alter(ContainerBuilder $container): void {
[...]
}

Service providers
●Named after the module machine
name:
○webprofiler ->
WebprofilerServiceProvider
○search_api ->
SearchApiServiceProvider
○core -> CoreServiceProvider
●In the root namespace of a module
For dynamic services
webprofiler/src/WebprofilerServiceProvider.php

22
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\webprofiler;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

class WebprofilerServiceProvider extends ServiceProviderBase {

public function register(ContainerBuilder $container): void {
[...]
}

public function alter(ContainerBuilder $container): void {
[...]
}

Service providers
●Use register method to:
○Add services that cannot be
easily defined using yaml
○Add services that must be
defined conditionally
○Register compiler passes
●Use alter method to:
○Remove or alter services
provided by Core or other
modules
For dynamic services
webprofiler/src/WebprofilerServiceProvider.php

23
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\webprofiler;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

class WebprofilerServiceProvider extends ServiceProviderBase {

public function register(ContainerBuilder $container): void {
$container->addCompilerPass(new ProfilerPass());
}

Service providers
A compiler pass is added to the process of
building the container itself.
For dynamic services
webprofiler/src/WebprofilerServiceProvider.php

24
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\Core;

class CoreServiceProvider
implements ServiceProviderInterface , ServiceModifierInterface {

public function register(ContainerBuilder $container) {
[...]
$container
->registerForAutoconfiguration (EventSubscriberInterface ::class)
->addTag('event_subscriber');
}

public function alter(ContainerBuilder $container): void {
[...]
}
Autoconfiguration
We can configure the container builder
process to automatically tag services that
implements a specific interface.
Aka registerForAutoconfiguration
core/lib/Drupal/Core/CoreServiceProvider.php

25
Autowiring all the things! DrupalCon Vienna 2025
services:
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
tags:
- { name: event_subscriber }
Autoconfiguration
This means that services that implement a
specific interface no longer need to be
individually tagged, you can just specify
autoconfigure: true in the _defaults
section of a module's services.yml and all
services will be automatically tagged.
From this
webprofiler/webprofiler.services.yml

26
Autowiring all the things! DrupalCon Vienna 2025
services:
_defaults:
autoconfigure: true

webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'

Autoconfiguration
In Core, services that implement those
interfaces are automatically tagged:
●MediaLibraryOpenerInterface (tag:
media_library.opener)
●EventSubscriberInterface
●(tag: event_subscriber)
●LoggerAwareInterface
●(tag: logger_aware)
●QueueFactoryInterface
●(tag: queue_factory)
To this
webprofiler/webprofiler.services.yml

27
Autowiring all the things! DrupalCon Vienna 2025
namespace Drupal\sm;

final class SmServiceProvider implements ServiceProviderInterface {

public function register(ContainerBuilder $container): void {
$container->registerForAutoconfiguration (TransportFactoryInterface ::class)
->addTag('messenger.transport_factory' );
$container->registerForAutoconfiguration (ServiceLocator::class)
->addTag('container.service_locator' );

Autoconfiguration
Custom and contrib modules can add more
mappings to
registerForAutoconfiguration
It works for your code too
sm/src/SmServiceProvider.php

28
DrupalCon Vienna 2025Autowiring all the things!
# Can be used on this:
tags:
- { name: event_subscriber }

# But not on this:
tags:
- {
name: data_collector,
template: '@webprofiler/Collector/database.html.twig' ,
id: 'database',
label: 'Database',
priority: 750,
}

Autoconfiguration
Autoconfiguration only works for tags that
doesn’t have attributes.
Limits

DrupalCon Vienna 2025Autowiring all the things!
Step 1
remove tag definition
29
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
tags:
- { name: event_subscriber }
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
From this:
To this:

DrupalCon Vienna 2025Autowiring all the things!
Step 2
remove arguments
30
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
From this:
To this:

31
Autowiring all the things! DrupalCon Vienna 2025
request_stack:
class: Symfony\Component\HttpFoundation\RequestStack
tags:
- { name: persist }
Symfony\Component\HttpFoundation\RequestStack : '@request_stack'
Aliases
Starting from Drupal 10.1 all core services have been
aliased with the Fully Qualified Class Name (FQCN) of the
service class.
Change record: https://www.drupal.org/node/3323122
REMEMBER: a service id is just a string:
●config.factory
●entity_type.manager
●Symfony\Component\HttpFoundation\RequestStack
●RequestStack::class
Alternate name for services
core/core.services.yml

32
Autowiring all the things! DrupalCon Vienna 2025
webprofiler.profiler:
class: Drupal\webprofiler\Profiler\Profiler
arguments:
[
'@webprofiler.file_storage' ,
'@logger.channel.webprofiler' ,
'@config.factory' ,
]
Drupal\webprofiler\Profiler\Profiler' : '@webprofiler.profiler'

Aliases
You can add FQCN to your module’s
services too.
Alternate name for services
webprofiler/webprofiler.service.yml

33
Autowiring all the things! DrupalCon Vienna 2025
use Drupal\webprofiler\Profiler\Profiler;
use Symfony\Component\HttpFoundation\RequestStack;

class ProfilerListener implements EventSubscriberInterface {

public function __construct(
private readonly Profiler $profiler,
private readonly RequestStack $requestStack,
)

Aliases
Now that all service IDs are the service
class FQCN, the service container
automatically resolves and injects
dependencies simply by matching the
requested FQCN.
This feature is called autowiring.

Alternate name for services
webprofiler/src/EventListener/ProfilerListener.php

34
Autowiring all the things! DrupalCon Vienna 2025
services:
_defaults:
autoconfigure: true
autowire: true

webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
Autowiring
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 or
for all services of a module.
Get rid of arguments
webprofiler/webprofiler.services.yml

35
Autowiring all the things! DrupalCon Vienna 2025
services:
_defaults:
autowire: true
autoconfigure: true

logger.channel.drupalcon2025 :
parent: logger.channel_base
arguments: ['drupalcon2025']

my_service:
class: Drupal\drupalcon2025\MyService

Autowiring
Usually a Drupal module extends the
logger.channel_base service to build a
new logger with the module name.
Both logger.channel_base,
logger.channel.drupalcon2025 and every
logger defined by Core and modules are
instances of the same interface:
Psr\Log\LoggerInterface.
Multiple services

36
Autowiring all the things! DrupalCon Vienna 2025
use Psr\Log\LoggerInterface;

class MyService {

public function __construct(
private readonly LoggerInterface $logger,
) {
}
}

Autowiring
As there are many services in the service
container that implements
Psr\Log\LoggerInterface, which specific
implementation will be injected here?
Multiple services

37
Autowiring all the things! DrupalCon Vienna 2025
37
Cool error messages courtesy of WebProfiler module ??????

38
Autowiring all the things! DrupalCon Vienna 2025
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyService {

public function __construct(
#[Autowire(service: 'logger.channel.drupalcon2025' )]
private readonly LoggerInterface $logger,
) {
}
}

Autowiring
Some services cannot be autowired,
usually because the interface is
implemented by multiple different services
(like loggers or cache bins in Drupal).
Use the #[Autowire] attribute to clearly
specify which service to inject.

Help autowire find services

39
Autowiring all the things! DrupalCon Vienna 2025
messenger.middleware.send_message:
class: Symfony\Component\Messenger\Middleware\SendMessageMiddleware
abstract: true
arguments:
$eventDispatcher: '@event_dispatcher'
calls:
- [setLogger, ['@logger.channel.sm']]
public: false

Named arguments
●A different technique to inject
arguments that cannot be
autowired is to use a named
argument
●Useful when you cannot change the
service class implementation
Help autowire find services
sm/sm.services.yml

40
Autowiring all the things! DrupalCon Vienna 2025
class SendMessageMiddleware implements MiddlewareInterface {
use LoggerAwareTrait;

public function __construct(
private SendersLocatorInterface $sendersLocator,
private ?EventDispatcherInterface $eventDispatcher = null,
private bool $allowNoSenders = true,
) {
}
}

Named arguments
The constructor parameter must have the
same name as the argument.
Help autowire find services

vendor/symfony/messenger/Middleware/SendMessageMiddleware.php

41
DrupalCon Vienna 2025Autowiring all the things!
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' ),
);
}

Autowiring in controller
A controller is not a service and it’s not
loaded by the service container.
The controller loader calls the create
method to build an instance of the
controller class.
The create method receives the whole
service container and create a new
instance of itself with required services.
This is an implementation of the service
locator anti-pattern.
webprofiler/src/Controller/DashboardController.php

42
DrupalCon Vienna 2025Autowiring all the things!
42
trait AutowireTrait {

public static function create(ContainerInterface $container) {
$args = [];

if (method_exists(static::class, '__construct')) {
$constructor = new \ReflectionMethod(static::class, '__construct');
foreach ($constructor->getParameters() as $parameter) {
$service = ltrim((string) $parameter->getType(), '?');
[...]
$args[] = $container->get($service);
}
}

return new static (...$args);
}
Starting from Drupal 10.2
ControllerBase use the AutowireTrait
trait.
AutowireTrait implements the create
method and automatically inject
required dependencies based on the
service FQCN.
namespace Drupal\Core\Controller;

abstract class ControllerBase implements ContainerInjectionInterface {

use AutowireTrait;
core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php
core/lib/Drupal/Core/Controller/ControllerBase.php
Autowiring in controller

43
DrupalCon Vienna 2025Autowiring all the things!
class DashboardController extends ControllerBase {

final public function __construct(
private readonly Profiler $profiler,
private readonly TemplateManager $templateManager,
) {
}
Autowiring in controller
create method is no longer needed.
This also works for any classes that
implement
ContainerInjectionInterface , for
example FormBase.
With upcoming Drupal 11.3 this will be
available for plugins too: Add create()
factory method with autowired parameters
to PluginBase (merged yesterday ?????? )
webprofiler/src/Controller/DashboardController.php

44
DrupalCon Vienna 2025Autowiring all the things!
namespace Drupal\file\Hook;

class FileViewsHooks {

use StringTranslationTrait ;

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 hooks
All hooks classes are automatically
exposed as autowired services.
core/modules/file/src/Hook/FileViewsHooks.php
44
Cool services list courtesy of WebProfiler module ??????

DrupalCon Vienna 2025Autowiring all the things!
Step 2
remove arguments
45
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
From this:
To this:

DrupalCon Vienna 2025Autowiring all the things!
Step 3
remove the id
46
webprofiler.profiler_listener :
class: Drupal\webprofiler\EventListener\ProfilerListener
Drupal\webprofiler\EventListener\ProfilerListener : ~
From this:
To this:

47
DrupalCon Vienna 2025Autowiring all the things!
\Drupal::service('Drupal\webprofiler\EventListener\ProfilerListener' );

// or:

\Drupal::service(ProfilerListener::class);

Remove the id
Services IDs are strings and classes FQCN
are strings too. We can drop the service ID
in almost any case.

48
DrupalCon Vienna 2025Autowiring all the things!
Drupal\webprofiler\EventListener\ProfilerListener : ~

# or:

Drupal\webprofiler\EventListener\ProfilerListener :

Remove the id
In YAML, a tilde (~) can be used to
represent the NULL value, but an empty
string is valid too (see YAML specs).
(I prefer the tilde version ?????? ).

DrupalCon Vienna 2025Autowiring all the things!
49
webprofiler.profiler_listener :
class:
Drupal\webprofiler\EventListener\ProfilerListener
arguments:
- '@webprofiler.profiler'
- '@request_stack'
- '@webprofiler.matcher.exclude_path'
tags:
- { name: event_subscriber }
_defaults:
autoconfigure: true
autowire: true

Drupal\webprofiler\EventListener\ProfilerListener : ~
From this:
To this:

Please rate this session
??????
50
DrupalCon Vienna 2025Autowiring all the things!

14 November, 2025 Sapienza University, Rome
drupalcampitaly.it

THANK YOU