Symfony architecture in production

Service container, HTTP lifecycle, Doctrine, security and cache: understanding how Symfony assembles the pieces is how you diagnose and evolve an application without breaking it.

Service container and compilation

The container is compiled once into cache: the result is a set of optimized PHP classes, not a dynamic resolution per request. Each service has visibility (`private` by default), a lifecycle and dependencies resolved by autowiring. To diagnose a problem, `debug:container` and the container dump are more reliable than intuition: they show the definition actually used.

# config/services.yaml
services:
    App\Quiz\Service\QuizAnswerService:
        arguments:
            $maxAttempts: '%env(int:QUIZ_MAX_ATTEMPTS)%'

The Kernel HTTP cycle

Each request flows through ordered events: `kernel.request`, controller resolution, `kernel.controller`, then `kernel.response`. Listeners let you decorate this cycle without duplicating logic in every controller. Mastering listener priorities and event propagation avoids hard-to-reproduce side effects, especially on error responses and negotiated formats.

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 and performance

The EntityManager holds an identity map: reading the same entity twice in one request returns the same instance, which guarantees consistency but hides changes made by other connections. `flush()` synchronizes all pending modifications; calling `flush()` in a loop multiplies round trips. For large reads, prefer read-only queries (`HYDRATE_ARRAY` or cursors) over loading full entities.

$rows = $entityManager->createQuery(
    'SELECT u.id, u.email FROM App\Account\Entity\User u WHERE u.active = :active'
)
    ->setParameter('active', true)
    ->setMaxResults(1000)
    ->getArrayResult();

Security: authentication, authorization and voters

Symfony security separates authentication (who are you?) from authorization (are you allowed?). Voters encode business rules per action and per resource; they are unit-testable without an HTTP request. For resources owned by a user, the voter must load the entity and check ownership, never trust an identifier passed in the 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();
    }
}