Logo de Symfony avec un petit fantôme heureux
Backend
12 minutes

Symfony 7.3 est disponible : que retenir de cette version ?

Publié le

Symfony 7.3 a débarqué le 29 mai avec de nouveaux composants et de nombreuses améliorations pour une DX aux petits oignons. On fait le tour ?

Avant d’accueillir la 8.0, prochaine version majeure de Symfony prévue pour le mois de novembre, cette itération 7.3 nous offre un joli florilège de nouveautés. Découvrons ensemble les principaux changements de cette version !

Trois nouveaux composants

Mapper des objets automatiquement

Jadis, transférer des données d’un objet à un autre, d’un DTO vers une entité par exemple, se faisait manuellement : une tâche répétitive, fastidieuse, et génératrice de beaucoup de code sans grande valeur.

Aujourd'hui, l’ObjectMapper est là pour nous simplifier la vie en automatisant ce transfert :

$product = $mapper->map($productDto, Product::class);

Cerise sur le clafoutis : le composant est hautement configurable grâce à l’attribut #[Map] que l’on place sur les propriétés de la classe source :

#[Map(target: Product::class)]
class ProductDto
{
    #[Map(target: 'name')]
    public string $productName;
}

Il est également possible d’ajouter des conditions ou des transformations personnalisées, pour affiner le comportement selon tes besoins.

À noter : ce composant est encore en statut expérimental. Son API pourrait donc évoluer sans attendre une version majeure.

Une alternative au Serializer

Le composant JsonStreamer offre une alternative plus simple mais également plus performante pour sérialiser des objets en JSON et inversement.

#[JsonStreamable]
class Product
{
    #[StreamedName('@id')]
    public string $reference;
}

Il offre également un système de transformeurs pour personnaliser le formatage des valeurs.

Explorer des données au format JSON

Le dernier des trois nouveaux composants, nommé JsonPath, permet de naviguer plus facilement dans des structures JSON en s'appuyant sur la syntaxe standardisée de la RFC 9535 :

$json = '{...}';
$crawler = new JsonCrawler($json);
$email = $crawler->find('$.user[0].email');

Cette syntaxe unifiée offre des capacités de filtrage et de recherche assez fines, idéales pour explorer des données JSON complexes !

Et si comme moi, tu adores les syntaxes objets, c'est possible aussi :

$json = '{...}';
$crawler = new JsonCrawler($json);

$path = new JsonPath();
$path = $path
    ->key('user')
    ->index(0)
    ->key('email')
;

$email = $crawler->find($path);

Une configuration toujours plus élégante

Symfony continue d’encourager la configuration via les attributs PHP, afin de l’alléger, la standardiser et la rapprocher du code concerné. Cette nouvelle version apporte d’ailleurs quelques améliorations dans ce sens.

Des commandes plus simples et lisibles

Le composant console se dote de deux nouveaux attributs : #[Argument] et #[Option] pour définir et récupérer facilement les arguments et les options de la commande sans passer par la méthode configure(). En complément l'attribut #[AsCommand] se dote d'un nouveau paramètre pour définir directement le texte d'aide de la commande.

#[AsCommand(
    name: 'app:export:product',
    description: 'Exports product data by reference',
    help: 'This command exports data for a given product reference',
)]
class ExportProductCommand
{
    public function __invoke(
        SymfonyStyle $io, 
        #[Argument] string $reference, 
        #[Option(description: 'Include all the prices by region')] bool $includePrices = false,
    ): int {
        // ...

        return Command::SUCCESS;
    }
}

Les extensions Twig évoluent

Les extensions Twig évoluent également pour simplifier leur déclaration. Depuis Symfony 7.3, il n’est plus nécessaire d’hériter de AbstractExtension, ni d’implémenter les méthodes getFilters() ou getFunctions().

Il suffit désormais d’utiliser les attributs #[AsTwigFilter] et #[AsTwigFunction] :

class PriceExtension
{
    #[AsTwigFilter('product_reference')]
    public function formatProductReference(string $reference): string
    {
        return sprintf('#%s', $reference);
    }
    
    #[AsTwigFunction('product_price')]
    public function getProductPriceByRegion(Product $product, Region $region): string
    {
        return '...';
    }
}

Encore mieux : plus besoin non plus de passer par des classes Runtime pour optimiser les performances, les extensions sont désormais chargées à la demande.

Des alias pour nos routes

Il est désormais possible de définir des alias pour les noms de route. Une nouveauté bien utile lorsqu’on souhaite faire évoluer sa stratégie de nommage en douceur, sans casser les anciennes références :

#[Route('/', name: 'main_index', alias: ['index', 'main_home'])]
public function index(): Response
{
    return new Response('Bienvenue');
}

Il est également possible de définir des alias pour les noms des paramètres de route, ce qui s’avère très pratique quand plusieurs propriétés d’objets portent le même nom :

#[Route('/blog/{categorySlug:category.slug}/{postSlug:post.slug}')]

Des améliorations simples mais efficaces

Vérifier les permissions de n'importe quel utilisateur

La méthode PHP isGranted() et son équivalent Twig is_granted() du SecurityBundle sont des outils redoutablement efficaces pour contrôler les permissions de l'utilisateur actuellement connecté. Cependant, dans certains cas, il est utile de pouvoir vérifier les permissions d'un utilisateur choisi de manière arbitraire.

Dans ce sens, une méthode PHP isGrantedForUser() a été ajoutée :

if ($this->security->isGrantedForUser($user, 'ROLE_ADMIN')) {
    $user->setGodMode(true);
}

Et bien sûr, son équivalent Twig is_granted_for_user est également disponible :

{% if is_granted_for_user(user, 'ROLE_PREMIUM_USER') %}
	<a href="#">Send Gift</a>
{% endif %}

Les voters s'expriment

Les voters sont la pierre angulaire des vérifications de permissions via la méthode isGranted(). Grâce à leur méthode voteOnAttribute(), ils renvoient un booléen pour autoriser ou non l’accès à une ressource.

Selon la complexité du système de permissions il est parfois nécessaire d'apporter un complément d'information au refus pour des besoins de débogage, de traçabilité ou d'expérience utilisateur.

Jusqu'à présent il fallait bricoler soi-même un mécanisme pour y parvenir. Cette nouvelle itération ajoute un paramètre Vote à la signature de la méthode voteOnAttribute() pour enrichir le refus d'un message :

protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{
    // ...
    
    if (false === $subject->isLocked()) {
        $vote?->addReason('The product is currently locked');

        return false;
    }
    
    if ($user !== $subject->getSeller()) {
        $vote?->addReason('Only the seller can update this product');

        return false;
    }
   
    return true;
}

Des améliorations pour les sorties de la console

Le composant Console continue de s’enrichir avec des améliorations des ses utilitaires de rendu.

Le Table Helper se dote d'un mode de rendu Markdown :

$table = new Table($output);
$table
    ->setHeaders(['Name', 'E-mail', 'Enabled'])
    ->setRows([
        ['Paul', 'paul@icier.fr', 'yes'],
        ['Marc', 'marc@opolo.es', 'no'],
    ])
;
$table->setStyle('markdown');
$table->render();

Un nouvel utilitaire Tree Helper permet d’afficher une arborescence très simplement :

$tree = TreeHelper::createTree($io, null, [
    'src' =>  [
        'Controller' => [
            'MainController.php',
        ],
        'Entity',
        'Kernel.php',
    ],
    'templates' => [
        'base.html.twig',
    ],
]);
$tree->render();
├── src
│   ├── Controller
│   │   └── MainController.php
│   ├── Entity
│   └── Kernel.php
└── templates
    └── base.html.twig

Mapper les données à partir d'une sous-clé

L’attribut MapQueryString permet d’hydrater automatiquement un objet à partir des paramètres présents dans l’URL. Dans Symfony 7.3, il devient encore plus pratique. On peut désormais cibler une sous-clé spécifique dans les paramètres :

#[Route('/search')]
public function __invoke(#[MapQueryString('filters')] SearchFilters $filters): Response
{
    // ...
}

Dédupliquer les messages dans Messenger

Messenger introduit un nouveau middleware pour dédupliquer les messages identiques dans la file d’attente. Une fonctionnalité bien pratique pour éviter de traiter plusieurs fois le même message et ainsi économiser des ressources ou prévenir des effets de bord indésirables.

$bus->dispatch(new UserReportMessage($user->getId()), [
    new DeduplicateStamp(sprintf('user-report-%s', $user->getId())),
]);

Et pour le coup, j’avais justement bricolé un système maison il y a quelques mois… c’était fastidieux, alors je suis franchement content de ne pas avoir à recommencer !

Paramètres de traductions globaux

Parfois, certains paramètres de traduction sont redondants à travers toute l’application. Les transmettre manuellement lors de chaque appel devient vite pénible. Nous pouvons maintenant ajouter des paramètres de traduction globaux dans le fichier de configuration config/packages/translator.yaml :

translator:
    # ...
    globals:
        '{app_name}': 'My Kitchen Feeder'
        '{contact_email}': 'contact@kitchen-feeder.io'

La performance n'est pas oubliée

Des pages d’erreurs statiques pour une meilleure résilience et performance

Symfony permet maintenant d'exporter les pages d'erreurs au format HTML grâce à la commande error:dump. Cela permet au serveur web de servir ces pages directement, sans passer par l’application, assurant ainsi un affichage rapide et fiable en toutes circonstances.

php bin/console error:dump public/error_pages

Pré-compression des ressources pour soulager le processeur

Le composant AssetMapper accueille désormais une fonctionnalité de pré-compression des ressources statiques, permettant de compresser les fichiers en amont, lors du déploiement par exemple, afin de réduire la charge CPU et servir ces ressources plus rapidement.

Pour activer ce comportement, il suffit d'ajouter la configuration suivante dans config/packages/asset_mapper.yaml :

 framework:
     asset_mapper:
         # ...
         precompress:
             format: 'gzip'

En plus de gzip, les formats brotli et zstandard sont également pris en charge.

Des dépréciations à anticiper avant la version 8

Dépréciation de la méthode eraseCredentials()

La méthode eraseCredentials() de l’interface UserInterface est désormais dépréciée. Elle servait à supprimer les infos sensibles stockées temporairement sur l’objet utilisateur. Symfony ne l’appelle plus automatiquement : alors sors ta serpillière, c'est à toi de faire le ménage.

Le passage des options de contraintes sous forme de tableau, c'est bientôt fini

À partir de cette version, passer les options des contraintes de validation sous forme de tableau est déprécié. Et franchement, ce n’est pas plus mal : pourquoi s’embêter avec un tableau quand on peut profiter des paramètres nommés ?

// Deprecated syntax
new Assert\Length([
    'min' => 3,
    'minMessage' => 'At least {{ limit }} characters',
]);

// Preferred syntax
new Assert\Choice(
    min: 3,
    minMessage: 'At least {{ limit }} characters',
);

Conclusion

Comme à l'accoutumée, cette nouvelle version de Symfony apporte son lot de nouveautés intéressantes. Pour garder cet article concis, j'ai sélectionné les nouveautés qui me semblaient les plus pertinentes. Cependant, Symfony 7.3 a bien plus à offrir. Si tu veux en savoir plus, tu peux retrouver l'ensemble des améliorations sur cet article du blog de Symfony.

Pour la suite des évolutions de notre framework PHP préféré, rendez-vous en novembre prochain avec la sortie simultanée de Symfony 8.0 et de la version 7.4 LTS.

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