In the last lesson, your service's constructor received three of Drupal core's own services — @database, @string_translation, and @messenger — without you ever writing a single line of wiring code. This lesson pulls back the curtain on exactly how that works: how arguments in your .services.yml lines up with your PHP constructor, and what happens when it doesn't.
What you'll learn in this lesson
- How the container matches each
@service.idargument to a constructor parameter, by position - How to depend on your own module's services, not just core's
- What actually happens when a requested service doesn't exist
- The difference between a hard dependency and an optional one
Arguments are matched by position, not by name
Revisit the real example from Examples module's dbtng_example.services.yml:
services:
dbtng_example.repository:
class: Drupal\dbtng_example\DbtngExampleRepository
arguments: ['@database', '@string_translation', '@messenger']
And the matching constructor in DbtngExampleRepository.php:
public function __construct(Connection $connection, TranslationInterface $translation, MessengerInterface $messenger) {
$this->connection = $connection;
$this->setStringTranslation($translation);
$this->setMessenger($messenger);
}
Notice there's no name matching happening here at all — the container simply resolves @database first and passes it as the first constructor parameter, then @string_translation as the second, and so on. Reorder the YAML list without reordering the constructor parameters (or vice versa) and you'll inject the wrong object into the wrong slot — Drupal won't warn you, since every argument still type-hints to a valid object, it's just the wrong one.
Depending on your own module's services
Nothing restricts arguments to core services — you can just as easily inject a service your own module already registered:
services:
my_module.formatter:
class: Drupal\my_module\MyFormatter
my_module.report_builder:
class: Drupal\my_module\ReportBuilder
arguments: ['@my_module.formatter', '@entity_type.manager']
ReportBuilder here depends on a service from its own module and a core service, side by side. The container doesn't care which module a service came from — only that it's registered somewhere in an enabled module.
@my_module.formatter before that service is actually defined (a typo in the ID, or the defining module isn't enabled), Drupal throws a ServiceNotFoundException the moment the container tries to compile — usually surfacing as a broken site the instant you run drush cr. This fails loudly and early, which is exactly what you want compared to a silent null value discovered deep in production.Quick check: you swap the order of two arguments in your
.services.ymlbut forget to swap the corresponding constructor parameters. Will Drupal throw an error when you clear the cache? (Answer: no — as long as every argument still satisfies its parameter's type-hint, the container happily injects the wrong-but-type-compatible object into each slot. This is exactly why argument order must be kept in careful sync with the constructor by hand.)
Key takeaways
argumentsin.services.ymlare matched to constructor parameters by position, never by name — keep the YAML order and the PHP parameter order in lockstep.- You can inject any registered service — core, contrib, or your own module's — the same way, with the same
@service.idsyntax. - A missing or misspelled service ID throws a
ServiceNotFoundExceptionat cache-rebuild time, not silently at runtime. - Because services are resolved once and shared, injecting one costs nothing extra even if many classes depend on it.
Coming up next
You've seen the YAML side of service definitions twice now — next we turn fully to the PHP side and build a complete, real service class from scratch, structuring its properties and methods the way Drupal core itself does.