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();
    }
}