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.
Prerequisites
Section titled “Prerequisites”- A custom module to work in. The first tutorial part gets you one.
- The services and dependency injection concept page explains what the container is; this page assumes that model.
- Familiarity with hooks and events helps you decide which one a given task needs.
Register a service
Section titled “Register a service”Create my_module.services.yml next to your .info.yml file. The complete file from the examples project:
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:
_defaultsapplies to every service in this file.autowire: truemeans constructor arguments are resolved from their type hints, so most services need nothing but their ID as the key and aclass:line.- Autowiring matches a type hint to a service through class-name aliases: core defines entries like
Drupal\Core\Database\Connection: '@database'incore.services.ymlfor exactly this purpose. When no alias exists for a type hint, name the argument explicitly, as the$loggerargument 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).
Alias the class name for autowiring
Section titled “Alias the class name for autowiring”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:
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 }Plug into core with service tags
Section titled “Plug into core with service tags”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:
| Tag | Your class implements | What it does |
|---|---|---|
event_subscriber | EventSubscriberInterface | Registers the service with the event dispatcher. |
access_check | AccessInterface | Makes the service available as a routing requirement through applies_to. |
logger | \Psr\Log\LoggerInterface | Receives every message logged anywhere on the site. |
http_client_middleware | (callable returning a Guzzle middleware) | Adds Guzzle middleware to the http_client service. |
needs_destruction | DestructableInterface | Gets 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.
Subscribe to an event
Section titled “Subscribe to an event”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:
<?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: trueFind events worth subscribing to
Section titled “Find events worth subscribing to”Event names are class constants. The ones you will meet most:
KernelEvents::REQUEST,KernelEvents::RESPONSE,KernelEvents::TERMINATEand the other request-lifecycle moments from Symfony’sKernelEvents. Core’s own subscribers incore/lib/Drupal/Core/EventSubscriber/are a gallery of what each one is good for.ConfigEventsfor configuration saves and imports.RoutingEventsto alter routes; extendRouteSubscriberBasefor that case.
Verify
Section titled “Verify”-
Run
drush cache:rebuildso the container picks up yourservices.yml. -
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-snippetsYou see
X-Docs-Snippets: hello. -
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. Ifdrush crcompletes, your definitions parsed.
Variations
Section titled “Variations”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.
Dispatch your own event
Section titled “Dispatch your own event”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).
Related
Section titled “Related”- Services and dependency injection is the concept page behind everything here.
- Hooks and events covers when to use which extension mechanism.
- Build your first module 3 builds and registers a service step by step.
- API topics:
services and dependency injection,events, andservice tags.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.