Services & Dependency Injection
A service in Symfony is nothing exotic — it's just a plain PHP class that does
one job well, such as sending an email or formatting a report. What makes Symfony's approach
powerful is the service container: an object that creates your service classes
and wires them together for you, so nothing in your codebase ever has to write
new SomeService() by hand.
A Simple Service Class
There's no special base class or interface required to make something a service. Any class that
focuses on a single responsibility qualifies — here's a small ArticleNotifier that
knows how to send a notification email whenever a new article is published.
<?php
namespace App\Service;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class ArticleNotifier
{
public function __construct(private MailerInterface $mailer)
{
}
public function notifyPublished(Article $article): void
{
$email = (new Email())
->to('editors@example.com')
->subject('New article published')
->text($article->getTitle());
$this->mailer->send($email);
}
}
Autowiring: Letting the Container Do the Work
The brief mention of autowiring back on the Controllers
page is exactly what's happening in the constructor above. ArticleNotifier simply
type-hints MailerInterface as a constructor argument, and it never has to instantiate
a mailer itself. When something asks the container for an ArticleNotifier, the
container inspects the constructor, sees it needs a MailerInterface, finds (or
builds) one, and passes it in automatically.
| Term | What it means |
|---|---|
| Service | A class with one focused job, managed by the container instead of instantiated manually |
| Service container | The object responsible for creating services and supplying their dependencies |
| Autowiring | The container reading a class's type-hinted constructor arguments and resolving them itself |
| Constructor injection | The pattern of declaring dependencies as constructor parameters instead of creating them inside the class |
Constructor Injection in a Controller
Controllers are services too, so they receive their dependencies the exact same way — just
type-hint ArticleNotifier in the controller's constructor (or directly on an action
method) and the container supplies an already-built instance, complete with its own
MailerInterface already wired in.
<?php
namespace App\Controller;
use App\Service\ArticleNotifier;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class ArticleController extends AbstractController
{
public function __construct(private ArticleNotifier $notifier)
{
}
public function publish(Article $article)
{
$article->setPublished(true);
$this->notifier->notifyPublished($article);
return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
}
}
Nowhere in this controller does new ArticleNotifier() or new Email()
ever appear — the class simply declares what it needs, and the container assembles the whole
object graph before the action runs.
Why This Matters: Testability
Because ArticleController only knows it needs something implementing the
shape of ArticleNotifier, a test can construct the controller with a fake notifier
that just records that notifyPublished() was called, instead of actually sending an
email. Nothing about ArticleController's own code has to change to make that swap —
constructor injection is what makes substituting a test double this easy.
ArticleNotifier service called from a command-line script instead of an HTTP request.