Retour au blog
Symfony

Architecture Symfony — 5 patterns qui changent tout

Hamza Chaouch

Hamza Chaouch

15 août 2026

8 min de lecture

Symfony est puissant. Mais avec grande puissance vient une grande responsabilité — celle de faire les bons choix d'architecture dès le départ.

J'ai vu des projets Symfony qui restent élégants même après 3 ans, et d'autres qui deviennent un marais en 6 mois. La différence n'est jamais la framework : c'est l'architecture.

1. Séparer les métiers avec Commands et Handlers

La grosse erreur : tout mettre dans les contrôleurs.

// ❌ Mauvais — logique métier dans le contrôleur
class OrderController extends AbstractController {
  #[Route('/order', methods: ['POST'])]
  public function create(Request $request, EntityManagerInterface $em): Response {
    $order = new Order();
    $order->setCustomer($request->get('customer'));
    $order->setTotal($request->get('total'));
    
    // Envoyer email
    $this->mailer->send(/* ... */);
    
    // Notifier partenaires
    $this->apiClient->notifyPartners($order);
    
    // Mettre à jour stock
    foreach ($order->getItems() as $item) {
      $item->getProduct()->decreaseStock($item->getQuantity());
    }
    
    $em->flush();
    return $this->json(['success' => true]);
  }
}
// ✅ Bon — déléguer au handler
class CreateOrderHandler {
  public function __construct(
    private EntityManagerInterface $em,
    private MailerInterface $mailer,
    private ApiClient $apiClient,
  ) {}

  public function handle(CreateOrderCommand $command): Order {
    $order = Order::create(
      $command->customerId,
      $command->items,
      $command->total,
    );

    // Notifier les autres systèmes *après* la persistance
    $this->em->flush();
    
    $this->mailer->send(/* ... */);
    $this->apiClient->notifyPartners($order);
    
    return $order;
  }
}

class OrderController extends AbstractController {
  #[Route('/order', methods: ['POST'])]
  public function create(
    Request $request,
    CommandBusInterface $commandBus,
  ): Response {
    $command = new CreateOrderCommand(/* ... */);
    $order = $commandBus->dispatch($command);
    return $this->json($order);
  }
}

Une règle simple : le contrôleur ne doit jamais connaître les détails du métier.

2. Eviter les services dépressifs

Un "Service Dump" est une classe qui contient 30 dépendances et fait 50 choses différentes.

// ❌ L'anti-pattern classique
class UserService {
  public function __construct(
    EntityManagerInterface $em,
    MailerInterface $mailer,
    ApiClient $apiClient,
    LoggerInterface $logger,
    CacheInterface $cache,
    // ... 15 dépendances de plus
  ) {}

  public function createUser(/* ... */) { /* ... */ }
  public function sendPasswordReset(/* ... */) { /* ... */ }
  public function notifyAdmin(/* ... */) { /* ... */ }
  public function synchronizeWithExternal(/* ... */) { /* ... */ }
  // ... 20 méthodes différentes
}

La solution : un service = une responsabilité.

// ✅ Bon — services focalisés
class UserRegistrationService {
  public function __construct(
    private EntityManagerInterface $em,
  ) {}

  public function register(RegisterUserCommand $command): User {
    // Juste créer l'utilisateur
  }
}

class PasswordResetNotifier {
  public function __construct(
    private MailerInterface $mailer,
  ) {}

  public function notifyReset(User $user): void {
    // Juste envoyer l'email
  }
}

3. Les Value Objects pour les données métier

Ne pas mélanger les strings simples avec les données importantes.

// ❌ Mauvais
class User {
  private string $email;
  
  public function setEmail(string $email): void {
    if (!str_contains($email, '@')) {
      throw new Exception('Invalid email');
    }
    $this->email = $email;
  }
}

// Ailleurs, oubli facile
$user->setEmail('invalid'); // Crash silencieux

// ✅ Bon — Value Object
class Email {
  private function __construct(private readonly string $value) {}

  public static function fromString(string $email): self {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
      throw new InvalidEmailException($email);
    }
    return new self($email);
  }

  public function __toString(): string {
    return $this->value;
  }
}

class User {
  private Email $email;
  
  public function setEmail(Email $email): void {
    $this->email = $email;
  }
}

// Validation forcée au type
$user->setEmail(Email::fromString('test@example.com'));

Conclusion

Symfony ne deviendra une usine à gaz que si tu en fais une usine à gaz.

La clé : des responsabilités claires, des services focalisés, des données typées. Le reste vient naturellement.

SymfonyPHPArchitectureBackendDesign Patterns
Hamza Chaouch

Hamza Chaouch

Lead Developer & Tech Lead — Conception et pilotage de plateformes web à fort enjeu métier.

Voir le profil →