Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.
Everything in a Symfony app is built around services and dependency injection. If those two words feel abstract, they should not, because the idea underneath is simple and it is the single most important habit in Symfony. A service is just an object that does a job, and dependency injection is the practice of handing an object the things it needs rather than letting it build them itself.
What a service actually is
A service is any object that performs a task: a mailer, a logger, a class that calculates an invoice, a repository that talks to the database. There is nothing special about the class itself. It becomes a service because the container knows how to create it and hand it to whoever needs it.
class InvoiceGenerator
{
public function generate(Order $order): Invoice
{
// ...do the work
}
}
That is a service. The point is that you never write new InvoiceGenerator() scattered around your app. You ask for it, and Symfony provides it.
Dependency injection is just asking
Dependency injection means a class receives its dependencies, usually through its constructor, instead of creating them. Compare the two. Here is a class that creates its own dependency:
class OrderProcessor
{
public function process(Order $order): void
{
$mailer = new Mailer(); // hard-wired, hard to test
$mailer->send(/* ... */);
}
}
And here is the same class with the dependency injected:
class OrderProcessor
{
public function __construct(private Mailer $mailer) {}
public function process(Order $order): void
{
$this->mailer->send(/* ... */);
}
}
The second version does not know or care how the Mailer was built. It just declares "I need a Mailer" in its constructor, and something else provides one. That something is the Symfony container.
Why this matters
Two payoffs make this worth doing everywhere. The first is testing. The injected version lets you pass a fake mailer in a test, so you can check the order was processed without actually sending email. The hard-wired version gives you no way in. The second is decoupling: OrderProcessor depends on the idea of a mailer, not on how to construct one, so you can swap the implementation without touching this class.
This is the foundation the rest of Symfony builds on. Controllers, commands, event listeners, all of them receive their dependencies the same way.
The container does the wiring
You declare what each class needs in its constructor, and the Symfony container figures out how to build the whole graph and hand each object its dependencies. You almost never construct services by hand. When OrderProcessor is created, the container sees it wants a Mailer, builds or reuses one, and passes it in.
How the container knows which class to provide for a given type-hint is the job of autowiring, which we cover in the service container and autowiring. For now, the mental model is enough: type-hint what you need, and the container supplies it.
Wrapping up
A service is an object that does a job, and dependency injection is the habit of receiving your dependencies through the constructor rather than building them yourself. It is what makes Symfony code testable and loosely coupled, and the container handles the actual wiring so you never write new for your services. Get comfortable with "declare what you need, let the container provide it" and the rest of Symfony stops feeling like magic and starts feeling like a pattern you already understand.
All comments ()
No comments yet
Be the first to leave a comment on this post.