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
BetterReader : arrête d'oublier les livres que tu as lus
BetterReader suit tes lectures, te fait reformuler et réviser ce que tu as appris, et te laisse partager ta carte mentale d'un livre avec d'autres lecteurs. Lire, retenir, progresser.
Jana-Home: الأذان والقرآن في قلب بيتك
تعرّف على Jana-Home، السماعة التي نطوّرها لترافق الأذان والاستماع إلى القرآن في بيتك مع تطبيق جَنى، ومساعد الذكاء الاصطناعي المخطط للإصدار القادم.