Architecture Symfony en production
Conteneur de services, cycle HTTP, Doctrine, sécurité et cache : comprendre comment Symfony assemble les briques, c’est savoir diagnostiquer et faire évoluer une application sans la casser.
Conteneur de services et compilation
Le conteneur est compilé une fois en cache : le résultat est un ensemble de classes PHP optimisées, pas une résolution dynamique à chaque requête. Chaque service a une visibilité (`private` par défaut), un cycle de vie et des dépendances résolues par autowiring. Pour diagnostiquer un problème, `debug:container` et le dump du conteneur sont plus fiables que l’intuition : ils montrent la définition réellement utilisée.
// config/services.yaml
services:
App\Quiz\Service\QuizAnswerService:
arguments:
$maxAttempts: '%env(int:QUIZ_MAX_ATTEMPTS)%'Le cycle HTTP du Kernel
Chaque requête traverse des événements ordonnés : `kernel.request`, résolution du contrôleur, `kernel.controller`, puis `kernel.response`. Les listeners permettent de décorer ce cycle sans dupliquer la logique dans chaque contrôleur. Maîtriser les priorités des listeners et la propagation des événements évite les effets de bord difficiles à reproduire, notamment sur les réponses d’erreur et les formats négociés.
final class CorsListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [KernelEvents::RESPONSE => ['onKernelResponse', 10]];
}
public function onKernelResponse(ResponseEvent $event): void
{
$event->getResponse()->headers->set('X-Content-Type-Options', 'nosniff');
}
}Doctrine : identity map, flush et performance
L’EntityManager tient une identity map : deux lectures d’une même entité dans une même requête renvoient la même instance, ce qui garantit la cohérence mais masque les changements faits par d’autres connexions. `flush()` synchronise toutes les modifications en attente ; appeler `flush()` dans une boucle multiplie les allers-retours. Pour les lectures volumineuses, privilégiez des requêtes en lecture seule (`HYDRATE_ARRAY` ou curseurs) plutôt que de charger des entités complètes.
$rows = $entityManager->createQuery(
'SELECT u.id, u.email FROM App\Account\Entity\User u WHERE u.active = :active'
)
->setParameter('active', true)
->setMaxResults(1000)
->getArrayResult();Sécurité : authentification, autorisation et voters
La sécurité Symfony sépare l’authentification (qui êtes-vous ?) de l’autorisation (avez-vous le droit ?). Les voters encodent les règles métier par action et par ressource ; ils sont testables unitairement sans requête HTTP. Pour les ressources possédées par un utilisateur, le voter doit charger l’entité et vérifier la propriété, jamais se fier à un identifiant passé dans l’URL.
final class QuizVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, ['view', 'delete'], true) && $subject instanceof QuizSession;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
return $subject->getOwner() === $token->getUser();
}
}