Understanding Dependency Injection
Understanding Dependency Injection
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
We've used autowiring SINCE chapter 8 (LoggerInterface, repositories, EntityManagerInterface), without naming the principle behind it explicitly. Time to fill in the foundation ALL of Symfony builds on.
The problem dependency injection solves
Imagine a class that creates its dependencies ITSELF:
class NotificationService
{
public function sendNotification(string $message): void
{
$logger = new \Monolog\Logger('app'); // hardwired!
$logger->info($message);
}
}Three concrete problems: the class is FIXED to ONE specific logger implementation (hard to swap out). It's HARD to test in isolation (chapter 42 needs a swappable fake logger for that). And if HOW the logger gets configured changes, EVERY spot that creates one itself must be updated.
The solution: passing dependencies in from outside
use Psr\Log\LoggerInterface;
class NotificationService
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function sendNotification(string $message): void
{
$this->logger->info($message);
}
}EXACTLY this pattern is already familiar from EVERY controller in this course – the class only requests an interface (LoggerInterface, not the concrete Logger class), WITHOUT knowing WHERE the actual implementation comes from. Providing that implementation is Symfony's service container's job.
The service container: Symfony's central factory
The container is a BIG directory: "if LoggerInterface is requested, provide THIS concrete instance." For EVERY use statement of a constructor parameter, Symfony automatically checks whether a matching service definition exists – EXACTLY this is autowiring.
php bin/console debug:containerLists ALL registered services – a good first stop when debugging "autowiring doesn't work" (usually: the sought-after service simply doesn't exist under the expected name).
php bin/console debug:autowiring LoggerSpecifically shows WHICH interfaces/classes with "Logger" in their name are available via autowiring – useful for finding the EXACT type for a constructor parameter.
Services are singletons by default
PER request, EVERY service gets instantiated ONLY ONCE – if ten different classes request LoggerInterface, ALL TEN get the same instance. This saves memory AND guarantees consistent state (e.g. a database connection opened once gets reused, instead of being rebuilt ten times).
Dependency injection vs. the service locator anti-pattern
Achtung: Resist the temptation to ask the container DIRECTLY for a service ($container->get(LoggerInterface::class)) – this hides a class's actual dependencies FROM the reader and makes testing harder. ALWAYS request dependencies explicitly via the constructor, as shown in this chapter – Symfony itself calls directly asking the container a "service locator" anti-pattern and explicitly advises against it.
Why this principle is so central to Symfony
Tipp: Dependency injection isn't just a detail – it's the reason Symfony code is maintainable in large teams: EVERY class documents its own dependencies (in the constructor), dependencies can be easily swapped for fakes in tests (block 7), and swapping an implementation (e.g. a different mailer provider in chapter 38) touches ONLY configuration, not the code that uses it.