SOLID, ce n’est pas un concept théorique pour impressionner en entretien. C’est un ensemble de 5 principes pratiques qui, appliqués à Symfony, rendent votre code testable, extensible et maintenable. Dans cette première partie d’une série de 4 articles sur l’architecture Symfony, on va voir chaque principe avec un exemple concret avant/après.
S — Single Responsibility Principle (SRP)
Un service ne doit avoir qu’une seule raison de changer. Le piège classique en Symfony : le service qui gère la création d’un utilisateur, l’envoi de l’email de bienvenue ET la mise à jour des statistiques.
<?php
// AVANT : Un service qui fait tout
class UserService
{
public function register(string $email, string $password): User
{
// Création utilisateur
$user = new User($email, $password);
$this->entityManager->persist($user);
$this->entityManager->flush();
// Envoi email
$this->mailer->send(new WelcomeEmail($user));
// Statistiques
$this->statsService->increment('registrations');
return $user;
}
}<?php
// APRÈS : Chaque responsabilité est isolée
#[AsMessageHandler]
final readonly class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepository,
private EventDispatcherInterface $eventDispatcher,
) {}
public function __invoke(RegisterUserCommand $command): void
{
$user = User::create(
Email::fromString($command->email),
);
$this->userRepository->save($user);
foreach ($user->pullDomainEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
}
}
// L'email et les stats sont gérés par des listeners sur UserCreatedLe handler ne fait qu’une chose : enregistrer l’utilisateur. L’email de bienvenue et les statistiques sont des effets secondaires gérés par des event listeners séparés.
O — Open/Closed Principle (OCP)
Un composant doit être ouvert à l’extension, fermé à la modification. Symfony excelle ici grâce à son système d’événements et de tags.
<?php
// Le système de notification est extensible sans modification
interface NotificationChannelInterface
{
public function send(Notification $notification, User $user): void;
public function supports(string $channel): bool;
}
final readonly class EmailChannel implements NotificationChannelInterface
{
public function send(Notification $notification, User $user): void
{
// Envoi par email
}
public function supports(string $channel): bool
{
return $channel === 'email';
}
}
// Pour ajouter Slack, SMS, etc. : juste une nouvelle classe.
// Aucune modification du code existant.Grâce au service tagging de Symfony et à l’autoconfiguration, chaque nouvelle implémentation de NotificationChannelInterface est automatiquement injectée. Zéro modification de config.
L — Liskov Substitution Principle (LSP)
Toute classe enfant doit pouvoir remplacer sa classe parent sans casser le comportement. En Symfony, cela se traduit par des interfaces bien définies et des contrats respectés.
<?php
// L'interface définit le contrat
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
}
// L'implémentation Doctrine respecte le contrat
final class DoctrineUserRepository extends ServiceEntityRepository
implements UserRepositoryInterface
{
public function save(User $user): void
{
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
public function findById(UserId $id): ?User
{
return $this->find($id->toString());
}
}
// Une implémentation InMemory pour les tests
final class InMemoryUserRepository implements UserRepositoryInterface
{
/** @var array<string, User> */
private array $users = [];
public function save(User $user): void
{
$this->users[$user->id()->toString()] = $user;
}
public function findById(UserId $id): ?User
{
return $this->users[$id->toString()] ?? null;
}
}Besoin d'un expert Symfony ?
Réserver un appel →Les deux implémentations sont interchangeables. Le service qui utilise UserRepositoryInterface ne sait pas (et n’a pas besoin de savoir) quelle implémentation est injectée.
I — Interface Segregation Principle (ISP)
Préférez plusieurs petites interfaces à une grosse. En Symfony, évitez les « God interfaces ».
<?php
// AVANT : Une interface trop grosse
interface UserServiceInterface
{
public function create(string $email): User;
public function update(UserId $id, array $data): User;
public function delete(UserId $id): void;
public function findById(UserId $id): ?User;
public function findAll(): array;
public function exportToCsv(): string;
public function sendWelcomeEmail(User $user): void;
}
// APRÈS : Des interfaces ciblées
interface UserWriterInterface
{
public function create(string $email): User;
public function update(UserId $id, array $data): User;
public function delete(UserId $id): void;
}
interface UserReaderInterface
{
public function findById(UserId $id): ?User;
public function findAll(): array;
}Un controller qui n’a besoin que de lire des utilisateurs ne dépend que de UserReaderInterface. Moins de dépendances = moins de fragilité.
D — Dependency Inversion Principle (DIP)
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions. C’est le fondement même de l’injection de dépendances de Symfony.
<?php
// Le Domain définit l'interface (abstraction)
// src/Domain/Repository/UserRepositoryInterface.php
interface UserRepositoryInterface
{
public function save(User $user): void;
}
// L'Infrastructure implémente (détail)
// src/Infrastructure/Doctrine/Repository/UserRepository.php
final class UserRepository implements UserRepositoryInterface
{
// Détail d'implémentation Doctrine
}
// Le Service dépend de l'abstraction, pas du détail
// src/Application/Handler/CreateUserHandler.php
final readonly class CreateUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepository, // abstraction !
) {}
}Le service applicatif n’a aucune idée que Doctrine existe. On peut remplacer la couche de persistance sans toucher à une seule ligne de logique métier.
SOLID + Symfony = architecture durable
Ces 5 principes ne sont pas des règles arbitraires. Ce sont des garde-fous éprouvés pour construire des applications qui supportent l’évolution dans le temps. Symfony les rend naturels grâce à son conteneur d’injection de dépendances, son système d’événements et son autoconfiguration. Dans les prochaines parties, on abordera l’architecture hexagonale, le CQRS et les patterns Doctrine avancés. Si vous cherchez à moderniser un projet Symfony existant, Bear Upgrade est un service de migration et de modernisation qui applique ces principes de manière systématique et automatise 80 % du travail avec Rector et Claude Code.
Besoin d'un expert Symfony ?
20 ans d'expérience sur l'écosystème PHP/Symfony.