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);
    }
}
Note: This is correct, real Symfony code meant to be copied into your own project — this website doesn't have Symfony installed, so nothing here runs live on this page.

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.

TermWhat it means
ServiceA class with one focused job, managed by the container instead of instantiated manually
Service containerThe object responsible for creating services and supplying their dependencies
AutowiringThe container reading a class's type-hinted constructor arguments and resolving them itself
Constructor injectionThe 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.

Next up: Head to Console Commands to see this same ArticleNotifier service called from a command-line script instead of an HTTP request.