Skip to content

How-to guideFor module developersDrupal 11

Services and events

Almost every class you write in a module becomes a service: an object the container constructs once, wires with its dependencies, and hands to whatever asks for it. This page shows you how to register services in *.services.yml, how to plug them into core systems with tags, and how to subscribe to events.

Your module’s classes are registered as container services with their dependencies injected, and at least one of them reacts to an event in the request lifecycle. If you want to react to something Drupal content does (a node is saved, a user logs in), you usually want a hook instead; see Hooks and events for how to choose, and Convert procedural hooks to classes for the hook workflow.

Create my_module.services.yml next to your .info.yml file. The complete file from the examples project:

web/modules/custom/docs_snippets/docs_snippets.services.yml
services:
# Opting into autowiring per file: services only need an entry, and
# constructor arguments are resolved from their type hints.
_defaults:
autowire: true
docs_snippets.response_subscriber:
class: Drupal\docs_snippets\EventSubscriber\ResponseSubscriber
tags:
- { name: event_subscriber }
docs_snippets.article_repository:
class: Drupal\docs_snippets\ArticleRepository
docs_snippets.article_manager:
class: Drupal\docs_snippets\ArticleManager
docs_snippets.welcome_mailer:
class: Drupal\docs_snippets\WelcomeMailer
# Not every argument can be autowired: pick a named logger channel.
arguments:
$logger: '@logger.channel.docs_snippets'
logger.channel.docs_snippets:
parent: logger.channel_base
arguments:
- 'docs_snippets'

What each part does:

  • _defaults applies to every service in this file. autowire: true means constructor arguments are resolved from their type hints, so most services need nothing but their ID as the key and a class: line.
  • Autowiring matches a type hint to a service through class-name aliases: core defines entries like Drupal\Core\Database\Connection: '@database' in core.services.yml for exactly this purpose. When no alias exists for a type hint, name the argument explicitly, as the $logger argument above does.
  • Service IDs are conventionally module_name.thing. Core increasingly registers services under their class name directly; both work.

Changes to a services.yml file only take effect after a container rebuild: run drush cache:rebuild (drush cr).

If other autowired code (including #[Hook] classes, which are autowired services themselves) should receive your service by type hint, add an alias from the class name to the service ID. The flagship tutorial module does this for its storage service, and it also shows a tagged service:

web/modules/custom/event_rsvp/event_rsvp.services.yml
services:
event_rsvp.storage:
class: Drupal\event_rsvp\RsvpStorage
arguments:
- '@database'
- '@datetime.time'
- '@cache_tags.invalidator'
# Alias the class name to the service so autowired classes — like the
# #[Hook] implementations in src/Hook — can receive it by type hint.
Drupal\event_rsvp\RsvpStorage: '@event_rsvp.storage'
event_rsvp.access_check:
class: Drupal\event_rsvp\Access\RsvpAccessCheck
arguments:
- '@config.factory'
tags:
# "applies_to" connects this service to the _event_rsvp_enabled
# requirement used in event_rsvp.routing.yml.
- { name: access_check, applies_to: _event_rsvp_enabled }

Tags mark a service as a participant in some core system. A compiler pass (code that runs once when the service container is compiled), or a service collector, finds every service with a given tag when the container is built and wires it in. The tags you will actually use, each validated against core:

TagYour class implementsWhat it does
event_subscriberEventSubscriberInterfaceRegisters the service with the event dispatcher.
access_checkAccessInterfaceMakes the service available as a routing requirement through applies_to.
logger\Psr\Log\LoggerInterfaceReceives every message logged anywhere on the site.
http_client_middleware(callable returning a Guzzle middleware)Adds Guzzle middleware to the http_client service.
needs_destructionDestructableInterfaceGets a destruct() call at the end of the request.
service_collector(the collecting service)Declares your own tag and collects services tagged with it.

The full inventory lives in the service tags topic. To define your own tag, give a service the service_collector tag (handled by TaggedHandlersPass) and other modules can register services into it.

Events are moments core (or another module) announces through the event_dispatcher service: a request arrived, a response is about to be sent, configuration was saved. A subscriber is a service implementing EventSubscriberInterface:

web/modules/custom/docs_snippets/src/EventSubscriber/ResponseSubscriber.php
<?php
declare(strict_types=1);
namespace Drupal\docs_snippets\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Adds a custom header to every response.
*
* Event subscribers react to moments in the request lifecycle (and to
* events other modules dispatch). Register the class as a service with
* the "event_subscriber" tag in docs_snippets.services.yml.
*/
class ResponseSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Event name => method to call. The optional integer is a priority;
// higher numbers run earlier.
return [
KernelEvents::RESPONSE => ['onResponse', 0],
];
}
/**
* Adds the header before the response is sent.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The response event, carrying the request and response objects.
*/
public function onResponse(ResponseEvent $event): void {
$event->getResponse()->headers->set('X-Docs-Snippets', 'hello');
}
}

getSubscribedEvents() maps event names to methods. The optional integer is a priority: higher runs earlier, the default is 0.

The example above carries an explicit event_subscriber tag so the wiring is visible. You can skip the tag entirely: CoreServiceProvider registers EventSubscriberInterface for autoconfiguration (Symfony’s mechanism for tagging services automatically by the interface they implement), so any service in a file that opts in gets the tag automatically. Core’s own System module relies on this; its subscribers carry no tags. Opt in next to autowire:

services:
_defaults:
autowire: true
autoconfigure: true

Event names are class constants. The ones you will meet most:

  • KernelEvents::REQUEST, KernelEvents::RESPONSE, KernelEvents::TERMINATE and the other request-lifecycle moments from Symfony’s KernelEvents. Core’s own subscribers in core/lib/Drupal/Core/EventSubscriber/ are a gallery of what each one is good for.
  • ConfigEvents for configuration saves and imports.
  • RoutingEvents to alter routes; extend RouteSubscriberBase for that case.
  1. Run drush cache:rebuild so the container picks up your services.yml.

  2. The subscriber above adds a header to every response. Check for it:

    Terminal window
    curl -sI https://my-site.ddev.site/ | grep -i x-docs-snippets

    You see X-Docs-Snippets: hello.

  3. Most mistakes in services.yml (malformed YAML, a reference to a service that does not exist) surface as an exception during the cache rebuild rather than silently. If drush cr completes, your definitions parsed.

Register or alter services in PHP with a service provider

Section titled “Register or alter services in PHP with a service provider”

When YAML is not enough (conditional registration, changing another module’s service), create a class named <CamelCaseModuleName>ServiceProvider in your module’s src/ directory. It is discovered by name alone, no registration needed. Implement ServiceProviderInterface to register services, ServiceModifierInterface to alter existing ones, or extend ServiceProviderBase for both:

// Fragment: in MyModuleServiceProvider (extends ServiceProviderBase).
public function alter(ContainerBuilder $container): void {
// Swap the class behind an existing service definition.
$container->getDefinition('cron')->setClass(MyCron::class);
}

Core itself bootstraps this way: CoreServiceProvider is a service provider that registers every core compiler pass.

Decorate a service instead of replacing it

Section titled “Decorate a service instead of replacing it”

Replacing a service’s class (as above) discards the original implementation and collides with any other module doing the same. Decoration wraps it instead. Drupal’s YAML loader supports Symfony’s decorates key:

my_module.cron:
class: Drupal\my_module\TimedCron
decorates: cron
arguments: ['@my_module.cron.inner']

The original cron service is renamed to my_module.cron.inner and injected into your wrapper, which now answers to the cron ID. Implement the same interface and forward the calls you do not change.

To let other modules react to something your module does, define an event class extending Event, give it a name constant, and dispatch it:

// Fragment: $this->eventDispatcher is the injected event_dispatcher.
$this->eventDispatcher->dispatch(new ArticleSyncedEvent($node), ArticleSyncedEvent::SYNCED);

Reach for this only for genuine lifecycle moments; for alterable data structures, hooks remain the Drupal-native pattern (see Hooks and events).

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.