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.

Hamza Chaouch
Lead Developer & Tech Lead — Conception et pilotage de plateformes web à fort enjeu métier.
Voir le profil →Articles similaires
Lovable à 13,3 Md$ : le vibe coding va-t-il remplacer les devs ?
Lovable lève 400M$ et double sa valorisation à 13,3 milliards de dollars. Le vibe coding remplace-t-il vraiment les développeurs ou redéfinit-il simplement le métier ?
Symfony sort son propre serveur LSP : la fin du monopole PhpStorm ?
Symfony Language Tools, le premier serveur LSP officiel du framework, démocratise l'autocomplétion et la navigation framework-aware en dehors de PhpStorm. Comment ça change la donne pour les développeurs ?