Symfony 8.0 est sorti le 27 novembre 2025, le même jour que 7.4. Il exige PHP 8.4 et abandonne tout ce qui était déprécié dans 7.4. Les deux changements les plus intéressants sont ce qu’il arrête de faire et ce qu’il commence à faire avec PHP 8.4.

Les objets paresseux natifs

Le système de proxy de Symfony, utilisé pour l’initialisation paresseuse des services et les proxies d’entités de Doctrine, a historiquement reposé sur la génération de code. Les classes proxy étaient générées au cache warmup, stockées sous forme de fichiers, et chargées à la demande. Ça fonctionnait, mais ça ajoutait une vraie complexité : des fichiers générés à gérer, un cache à invalider, du code qui ne ressemblait en rien à la classe qu’il proxyifiait.

PHP 8.4 a ajouté des objets paresseux natifs. Symfony 8.0 les utilise. Le LazyGhostTrait et le LazyProxyTrait qui alimentaient l’ancien système sont supprimés. La création de proxy est maintenant une opération à l’exécution soutenue par le moteur lui-même, pas une étape de génération de code.

Pour les développeurs d’applications, le changement est essentiellement invisible : les services paresseux fonctionnent toujours. Pour les auteurs de frameworks et bibliothèques, une surface significative de complexité vient de disparaître.

FormFlow

Les formulaires multi-étapes ont toujours été un exercice DIY dans Symfony. Gestion de session, suivi des étapes, validation partielle, navigation entre les étapes : chaque projet roulait sa propre solution ou importait un bundle tiers.

8.0 introduit FormFlow : un mécanisme intégré pour les wizards de formulaires multi-étapes. Les étapes sont définies comme une séquence de types de formulaires, la validation partielle est scopée à l’étape courante, et la gestion de session est gérée automatiquement.

#[AsFormFlow]
class CheckoutFlow extends AbstractFormFlow
{
    protected function defineSteps(): Steps
    {
        return Steps::create()
            ->add('shipping', ShippingType::class)
            ->add('payment', PaymentType::class)
            ->add('review', ReviewType::class);
    }
}

La config XML et PHP fluent supprimées

La dépréciation de 7.4 du format de configuration PHP fluent devient une suppression définitive dans 8.0. La configuration XML sort aussi comme format de première classe. Les formats supportés pour la configuration applicative sont maintenant YAML et tableaux PHP. L’empreinte rétrécit, mais ce qui reste est genuinement meilleur.

Ce qui est supprimé d’autre

  • Le support PHP 8.2 et 8.3 (8.4 minimum)
  • ContainerAwareInterface et ContainerAwareTrait
  • L’usage interne de LazyGhostTrait et LazyProxyTrait par Symfony
  • La surcharge de méthode HTTP pour GET et HEAD (seul POST a du sens sémantiquement)

Symfony 8.0 est une rupture propre, et ce genre de rupture ne devient possible que quand le plancher PHP s’élève. Les objets paresseux de PHP 8.4 sont l’exemple le plus clair : la fonctionnalité existe maintenant dans le langage, donc le framework peut simplement arrêter de l’implémenter.

Console devient plus ergonomique pour les commandes invocables

Les commandes invocables reçoivent une mise à niveau significative. L’attribut #[Input] transforme un DTO en bag d’arguments/options de la commande. Fini d’appeler $input->getArgument() dans le handler :

#[AsCommand(name: 'app:send-report')]
class SendReportCommand
{
    public function __invoke(
        #[Input] SendReportInput $input,
    ): int {
        // $input->email, $input->dryRun, etc.
        return Command::SUCCESS;
    }
}

BackedEnum est supporté dans les commandes invocables, donc une option déclarée comme enum Status est validée et castée automatiquement. Les commandes interactives reçoivent les attributs #[Interact] et #[Ask] pour déclarer les prompts de questions en ligne. CommandTester fonctionne avec les commandes invocables sans câblage supplémentaire.

Le Routing trouve ses propres contrôleurs

Les routes définies via #[Route] sur les classes de contrôleurs sont auto-enregistrées sans avoir besoin d’une entrée resource: explicite dans config/routes.yaml. Le tag routing.controller est appliqué automatiquement. On contrôle toujours quels répertoires sont scannés, mais la config YAML rétrécit jusqu’à un pointeur vers un répertoire plutôt qu’une liste de fichiers manuelle.

#[Route] reçoit aussi un paramètre _query pour définir des paramètres de query à la génération, et plusieurs environnements dans l’option env.

Sécurité : CSRF et OIDC reçoivent de meilleurs outils

#[IsCsrfTokenValid] reçoit un argument $tokenSource pour spécifier d’où vient le token (header, cookie, champ de formulaire) plutôt que de s’appuyer sur une convention fixe. SameOriginCsrfTokenManager ajoute la validation du header Sec-Fetch-Site, un mécanisme de protection CSRF natif au navigateur qui n’a pas besoin d’injection de token du tout.

La commande security:oidc-token:generate crée des tokens pour tester les endpoints protégés OIDC en local. Plusieurs endpoints de découverte OIDC sont maintenant supportés, utile dans les setups multi-tenant où chaque tenant a son propre identity provider.

Deux nouvelles fonctions Twig : access_decision() et access_decision_for_user() exposent le résultat du voter d’autorisation dans les templates sans passer par la façade de sécurité. #[IsGranted] peut être sous-classé pour les patterns d’autorisation répétés qui méritent leur propre attribut nommé.

ObjectMapper et JsonStreamer sortent d’expérimental

Les deux composants introduits dans 7.x sont stables dans 8.0. ObjectMapper mappe entre objets sans transformateurs écrits à la main, via une configuration basée sur des attributs. JsonStreamer lit et écrit de grands JSON sans charger le document entier en mémoire, et il supporte maintenant les propriétés synthétiques : des champs virtuels calculés à la sérialisation.

JsonStreamer abandonne aussi sa dépendance sur nikic/php-parser. La génération de code pour le reader/writer utilise maintenant un mécanisme interne plus simple, supprimant une lourde dépendance de dev.

Uid par défaut vers UUIDv7

UuidFactory génère maintenant UUIDv7 par défaut au lieu d’UUIDv4. La différence : v7 est ordonné dans le temps, donc les UUIDs générés se trient chronologiquement. Ça compte beaucoup pour la performance des index de base de données. MockUuidFactory fournit une génération déterministe d’UUID dans les tests.

Yaml lève une erreur sur les clés dupliquées

Auparavant, un fichier YAML avec deux clés identiques gardait silencieusement la dernière. 8.0 lève une erreur de parsing. Ça attrape de vrais bugs : les clés dupliquées dans services.yaml ou config/packages/*.yaml sont presque toujours des erreurs de copier-coller et on veut définitivement être informé.

Validator : contrainte Video et protocoles wildcard

Une contrainte Video rejoint la contrainte Image pour valider les fichiers vidéo uploadés (type MIME, durée, codec). La contrainte Url accepte protocols: ['*'] pour autoriser n’importe quel schéma conforme à la RFC 3986, utile pour stocker des URLs arbitraires qui incluent git+ssh://, file://, ou des schémas d’application personnalisés.

Messenger : retry natif SQS et nouveaux événements

Le transport SQS peut maintenant utiliser sa propre configuration native de retry et de dead-letter queue au lieu du middleware de retry de Symfony. Pour les queues à haut volume sur AWS, ça supprime un aller-retour par PHP pour les échecs transitoires. Un MessageSentToTransportsEvent se déclenche après qu’un message est dispatché, portant des informations sur quels transports l’ont réellement reçu.

messenger:consume reçoit --exclude-receivers pour se combiner avec --all.

Mailer : transport Microsoft Graph

Un nouveau transport envoie des emails via l’API Microsoft Graph, ce que Microsoft recommande pour les applications sur Azure Active Directory ces jours-ci. Les autres options (relai SMTP, Exchange EWS) fonctionnent encore, mais Graph est le bon choix pour les nouveaux déploiements Azure.

Workflow : transitions pondérées

Les transitions peuvent maintenant déclarer des poids. Quand plusieurs transitions sont activées depuis la même place, celle avec le poids le plus élevé gagne. Ça permet d’exprimer la priorité directement dans la définition du workflow sans ajouter un guard qui lit un compteur externe.

return (new Definition(states: ['draft', 'review', 'published']))
    ->addTransition(new Transition('publish', 'review', 'published', weight: 10))
    ->addTransition(new Transition('reject', 'review', 'draft', weight: 1));

Lock : LockKeyNormalizer

LockKeyNormalizer normalise une clé de lock vers une string cohérente avant le hachage. Utile quand la clé est dérivée d’entrées utilisateur ou de données externes qui peuvent varier en espaces blancs ou casse : le normalizer s’assure que la même clé logique correspond toujours au même lock.

HttpFoundation : méthode QUERY et parsing de corps plus propre

La méthode IETF QUERY (une méthode sûre et idempotente avec un corps, contrairement à GET) est maintenant supportée partout dans la stack : Request, cache HTTP, WebProfiler et HttpClient. Si on construit des APIs de recherche qui nécessitent un corps de requête structuré et veulent aussi du cache, QUERY est le bon choix sémantique.

Request::createFromGlobals() parse maintenant automatiquement le corps des requêtes PUT, DELETE, PATCH et QUERY.

Config : schéma JSON pour la validation YAML

Symfony 8.0 auto-génère un fichier JSON Schema pour chaque section de configuration. Les IDEs qui supportent JSON Schema pour les fichiers YAML (VS Code, PhpStorm) peuvent maintenant valider config/packages/*.yaml contre ces schémas et fournir de l’autocomplétion sans plugin. Le schéma est généré pendant le cache warmup et placé dans config/reference.php.

Runtime : auto-détection FrankenPHP

Le composant Runtime détecte FrankenPHP automatiquement et active le mode worker sans package supplémentaire ni variable d’environnement. Si $_SERVER['APP_RUNTIME'] est défini, cette classe de runtime a la priorité. On peut aussi choisir le renderer d’erreurs basé sur APP_RUNTIME_MODE, ce qui est utile quand on fait tourner la même codebase dans des contextes HTTP et CLI avec des besoins de présentation d’erreurs différents.