Logo de Symfony
Backend
17 minutes

À la découverte de Symfony 8

Publié le

Les versions 7.4 et 8 de Symfony sont arrivées le 27 novembre dernier : le framework made in France poursuit sa modernisation et son amélioration continue.

C’est Noël avant l’heure pour la communauté Symfony, avec l’arrivée simultanée des versions 7.4 et 8. Pas de révolution en soi, mais plutôt un prolongement de la dynamique du framework en apportant des améliorations concrètes et des nouveautés qui simplifient la vie des développeurs. Passons en revue les évolutions clés 🚀.

Pourquoi deux versions en simultané ?

Si tu fréquentes l’écosystème Symfony depuis longtemps, tu le sais sûrement déjà. Cependant, pour les petits nouveaux, il est important de faire le point sur le rythme des mises à jour de notre framework préféré.

Tous les deux ans, au mois de novembre, la Core Team du projet propose une sortie simultanée de la version n.4 et de la n+1.0. Ces deux versions sont quasi identiques : mêmes fonctionnalités, mêmes évolutions, mêmes optimisations… à deux détails près.

Dans le cas présent, la version 7.4 offre un support à long terme avec des correctifs jusqu’à 2 ans après sa sortie et des correctifs de sécurité jusqu’à 3 ans. La version 8, quant à elle, propose un support plus court, jusqu’en juillet 2026.

La version 8 est également débarrassée de toutes les dépréciations apparues durant le cycle de vie de la version 7. En théorie, n’importe quelle application en 7.x peut être migrée vers la 7.4 les yeux fermés. Toutefois, reste quand même attentif car théorie et pratique ne cohabitent pas toujours facilement. En revanche, la version 8 applique définitivement les changements ayant un impact sur ton code, il faut donc s’assurer d’éliminer toutes les dépréciations présentes en 7.4 avant de migrer.

Alors, quelle version choisir ?

Si tu démarres une nouvelle application :

  • Tu as l’intention de mettre à jour ton application régulièrement ? Opte pour la 8.
  • Tu préfères bénéficier d’un support longue durée pour laisser vivre ton application tranquillement ? La 7.4 est faite pour toi !

Si tu mets à jour une application existante :

  • Passe d’abord par la 7.4 pour éliminer les éventuelles dépréciations.
  • Puis, bascule sur la 8 si le support court terme ne te pose pas de problème.

D’ailleurs, si tu veux garder un œil sur les versions actuellement supportées, je t’invite à consulter la roadmap officielle du projet.

Enfin des formulaires multi-étapes dans Symfony

Pour moi, le changement le plus marquant de ces versions est la possibilité de créer des formulaires multi-étapes de manière native, il était temps !

Le composant de gestion des formulaires est probablement l’un des plus complets du framework, mais il souffrait jusque-là d’une lacune : il n’offrait aucun moyen natif pour créer des formulaires en plusieurs étapes. Jusqu’à présent, les développeurs devaient, à chaque projet nécessitant un formulaire découpé en plusieurs phases, mettre en place un système maison, souvent lourd à développer et difficile à maintenir.

Cette époque est désormais révolue avec l’arrivée des Form Flow : nous disposons maintenant d’une solution générique et prête à l’emploi pour ce type de fonctionnalités !

Cerise sur le gâteau : si tu maîtrises déjà les formulaires Symfony, la mise en place des Form Flow est un jeu d’enfant.

Un objet pour les contrôler tous

La première étape consiste à mettre en place une entité ou un DTO pour accueillir nos données :

class Order
{
    #[Assert\Valid(groups: ['subscription']]
    public Subscription $subscription;
    
    #[Assert\Valid(groups: ['billing'])]
    public Billing $billing;
    
    #[Assert\Valid(groups: ['payment'])]
    public Payment $payment;
    
    public string $step = 'subscription'; 
    
    public function __construct() {
        $this->subscription = new Subscription();
        $this->billing = new Billing();
        $this->payment = new Payment();
    }
}
  • Les groupes de validation sur chacune des propriétés permettent une validation partielle à chaque étape.
  • La propriété $step sert à indiquer l’état d’avancement de l’utilisateur. Ici, pour simplifier, j’utilise une simple chaîne de caractères, mais je recommande d’opter pour une énumération afin d’avoir un code plus propre et robuste.

Le FormFlow, le cœur du formulaire

Il est maintenant temps de créer un OrderType qui hérite de AbstractFlowType pour organiser nos étapes :

class OrderType extends AbstractFlowType
{
    public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
    {
        $builder->addStep('subscription', SubscriptionType::class);
        $builder->addStep('billing', BillingType::class);
        $builder->addStep('payment', PaymentType::class);

        $builder->add('navigator', NavigatorFlowType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Order::class,
            'step_property_path' => 'step',
        ]);
    }
}
  • Chaque appel à $builder->addStep() ajoute une nouvelle étape à notre formulaire.
  • L’ajout d’un champ NavigatorFlowType fournit automatiquement les boutons de navigation entre les étapes.
  • L’option step_property_path doit pointer vers la propriété $step de notre classe Order, afin que le formulaire suive correctement l’avancement de l’utilisateur.

Un contrôleur familier

Pour finir, mettons en place le contrôleur, presque identique à celui que l’on utiliserait pour un formulaire classique :

class OrderController extends AbstractController
{
    #[Route('/order', name: 'app_order_form')]
    public function form(Request $request): Response
    {
        $order = new Order();
        $flow = $this->createForm(OrderType::class, $order);
        $flow->handleRequest($request);

        if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) {
            $this->em->persist($order);
            $this->em->flush();

            return $this->redirectToRoute('app_order_list');
        }

        return $this->render('order/form.html.twig', [
            'form' => $flow->getStepForm(),
        ]);
    }
}
  • La méthode $flow->isFinished() permet de déterminer si toutes les étapes ont été parcourues et si l’on peut passer au traitement des données.
  • La méthode $flow->getStepForm() récupère le formulaire de l’étape courante, à transmettre à la vue pour affichage.

Ce nouveau moyen de créer des formulaires multi-étapes semble prometteur et j'ai hâte de le mettre en œuvre en condition réelle !

Un système de gestion des autorisations toujours plus complet

Depuis plusieurs itérations, Symfony améliore son mécanisme d’autorisations afin de le rendre plus complet et de permettre aux développeurs d’offrir des retours plus clairs et plus pertinents à l’utilisateur.

Des méta-données sur le vote

Comme je l’écrivais dans mon article sur la version 7.3, Symfony fournit désormais à la méthode voteOnAttribute() de nos voters un objet Vote, qui permet de détailler la raison d’un refus d’accès à une ressource. Il est maintenant également possible d’ajouter des méta-données complémentaires via la propriété extraData, pour enrichir le retour utilisateur ou mieux gérer ce refus ailleurs dans l’application.

protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{
	if (false === $subject->isOutdated()) {
        $vote?->addReason('This post revision is outdated');
        $vote?->extraData['nextRevision'] = $subjet->getNextRevision();

        return false;
    }
   
    return true;
}

Afficher plus d'informations à l'utilisateur en cas de refus d'accès

Nous pouvons désormais ajouter une raison et des méta-données lors d’un refus d’accès, mais à quoi ça sert ? Les développeurs de Symfony ont répondu à cette question en implémentant une nouvelle fonction Twig : access_decision, qui permet de récupérer facilement ces informations directement dans les templates.

{% set decision = access_decision('post_revision_edit', post) %}
{% if decision.isGranted() %}
	{{ post.title }}
{% else %}
    <p>{{ decision.message }}</p>
    {% set vote = decision.votes|first %}
    {% if vote.extraData.nextRevision is defined %}
        <a href="{{ path('post_revision_edit', { 
        	id: vote.extraData.nextRevision.id,
        }) }}">
            Aller à la révision suivante
        </a>
    {% endif %}
{% endif %}

Limiter le contrôle d'accès à certaines méthodes HTTP

L’attribut #[IsGranted] dispose désormais d’un nouveau paramètre method. Ce dernier permet d’appliquer le contrôle d’accès uniquement à certaines méthodes HTTP, ce qui peut s’avérer particulièrement utile, notamment dans le cas d’une API.

#[IsGranted('ROLE_USER', methods: ['GET'])]
#[IsGranted('ROLE_ADMIN', methods: ['POST'])]
public function publish(): Response
{
    // ...
}

De gros changement dans la configuration

Dépréciation des configurations en XML

Symfony permet depuis ses débuts de réaliser tes configurations au format XML, mais ce format est en perte de vitesse : les développeurs lui préfèrent aujourd’hui YAML ou PHP.

Aujourd’hui, une page se tourne : Symfony 7.4 déprécie ce format et supprime définitivement son support dans la version 8. Avant de migrer vers cette prochaine version majeure, assure-toi donc de convertir tes éventuelles configurations XML en YAML ou PHP. Il faudra également vérifier que tous les bundles utilisés ont bien été mis à jour pour gérer cette transition.

D’ailleurs, je crains que ce soit le principal frein pour passer en version 8 pour certains projets reposant sur des bundles peu ou plus maintenus 😱.

Une meilleure auto-complétion en YAML

Une faute de frappe est vite arrivée quand on écrit de la configuration en YAML, et là… boum : une exception qui nous indique que le paramètre sevices n’existe pas ! Jusque-là, nos IDE ne proposaient qu’une auto-complétion partielle, voire inexistante.

Grâce au schéma JSON, c’est désormais de l’histoire ancienne : une simple ligne en haut de nos fichiers YAML suffit pour obtenir une auto-complétion fiable et exhaustive, dès lors que le schéma est fourni par la dépendance.

Alors si, comme moi, tu es du genre à supprimer tous les commentaires par défaut dans les fichiers de configuration, garde au moins ceux-ci :

# yaml-language-server: $schema=...

Retour aux tableaux pour la configuration PHP

Après être passée des tableaux aux objets en version 5.3, la configuration PHP revient à ses origines, en dépréciant le format orientée objet pour revenir aux bons vieux tableaux.

Ce qui pourrait être perçu comme un retour en arrière est en réalité motivé par des difficultés techniques avec le format orienté objet, notamment pour la mise à jour automatique via les recettes Symfony. Mais, ce n’est pas vraiment un retour en arrière : grâce au nouveau fichier config/reference.php, nous bénéficions désormais d’une auto-complétion complète dans nos tableaux de configuration.

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'pattern' => '^/*',
                'lazy' => true,
                'anonymous' => true,
            ],
        ],
        'access_control' => [
            ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
        ],
    ]
]);

D'autres nouveautés qui valent le coup d’œil

Simplifier la validation des vidéos lors du téléversement

Une nouvelle contrainte Video fait son apparition. Comme son nom l’indique, et à l’instar de la contrainte Image, elle permet de valider une vidéo téléversée : son format, sa résolution, son ratio d’affichage...

class Clip
{
    #[Assert\Video(
        minWidth: 800,
        minHeight: 600,
        maxSize: '50M',
        mimeTypes: ['video/mp4', 'video/webm'],
    )]
    private File $sourceFile;
}

Tu peux retrouver l’ensemble des options dans la documentation officielle.

⚠️ L’installation de FFmpeg est requise pour pouvoir utiliser cette contrainte.

Une meilleure gestion des exceptions dans le terminal

Si tu écris des tests, tu as sûrement déjà rencontré des exceptions affichées en HTML brut dans ton terminal, qui sont plutôt cryptiques et peu pratiques. C’était jusque-là un aspect frustrant de l’écriture des tests sur Symfony.

Bonne nouvelle, c’est désormais terminé grâce à l’option de configuration kernel.runtime_mode, définie automatiquement à l’exécution. En l'utilisant, les exceptions s’affichent toujours en HTML dans le navigateur mais en texte brut dans le terminal.

Après si tu es du genre cryptique et mystérieux, tu peux toujours revenir à l’ancien comportement en ajustant la variable d’environnement APP_RUNTIME_MODE.

Des énumérations dans mes workflows ?

Je ne t’ai jamais dit que j’étais un super-héros ? Je suis EnumMan, j’adore les énumérations : elles rendent le code plus clair, plus sûr et plus facile à maintenir.

Alors, quand j’ai commencé à utiliser le composant Workflow, j’ai voulu définir les différents états avec une énumération... Sauf que ce n’était pas supporté nativement. Plutôt que de renoncer, parce qu’EnumMan ne renonce jamais, j’ai bricolé une solution. Elle fonctionnait, mais ça restait un simple ruban adhésif.

Aujourd’hui, je suis rempli de joie ! Fini les bricolages, les workflows supportent enfin les énumérations nativement !

framework:
    workflows:
        order_workflow:
            type: 'workflow'
            marking_store:
                type: 'method'
                property: 'status'
            supports:
                - App\Entity\Order
            initial_marking: !php/enum App\Enum\OrderStatus::Draft
            places: !php/enum App\Enum\OrderStatus
            transitions:
                validate:
                    from: !php/enum App\Enum\OrderStatus::Draft
                    to: !php/enum App\Enum\OrderStatus::Validated
                shipping:
                    from: !php/enum App\Enum\OrderStatus::Validated
                    to: !php/enum App\Enum\OrderStatus::Shipped
                closing:
                    from: !php/enum App\Enum\OrderStatus::Shipped
                    to: !php/enum App\Enum\OrderStatus::Closed

Dépréciation de la méthode get de la classe Request

La méthode $request->get('key') est un raccourci qui permet de chercher une valeur associée à la clé passée en paramètre. Elle recherche à la fois dans les attributs, les paramètres POST et les paramètres GET de la requête.

Je dois vous avouer une chose : j’ai toujours trouvé cette méthode un peu inutile et contre-productive. Pourquoi fouiller dans ces trois endroits à la fois ? Il vaut mieux aller directement au bon endroit, comme par exemple $request->query->get('key') pour un paramètre GET.

Bonne nouvelle : cette méthode est dépréciée en version 7.4 et supprimée en 8.0. Enfin, "bonne nouvelle"… à condition de ne pas l’avoir utilisée à outrance, sinon bon courage pour supprimer toutes ces dépréciations !

Conclusion

Dans cet article, nous avons passé en revue les nouveautés les plus intéressantes du framework mais Symfony 7.4/8 a encore beaucoup à offrir. Tu peux retrouver la liste complète sur le blog officiel.

Cette version est, selon moi, une très bonne itération. Sans révolutionner notre approche du framework, elle apporte son lot de nouveautés pertinentes, mettant de plus en plus l’expérience développeur au centre des priorités. Le tout en évoluant avec les bonnes pratiques actuelles et en ajoutant toujours plus de fonctionnalités pour des développements plus rapides et efficaces.

À bientôt sur ce blog ou en live sur Twitch 👋.