Symfony 5.4 est sorti le 29 novembre 2021, le même jour que Symfony 6.0 et un jour après PHP 8.1. Pas un hasard.
5.4 est la version LTS, et son rôle est de porter autant que possible le jeu de fonctionnalités de 6.0 tout en conservant la compatibilité 5.x. C’est aussi la première version de Symfony qui comprend réellement les fonctionnalités de PHP 8.1.
Support des enums
PHP 8.1 a introduit les enums natifs. Symfony 5.4 les embrasse immédiatement :
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
Le type de formulaire EnumType rend les enums sous forme de listes déroulantes, sans transformateurs personnalisés. Le validateur comprend les backed enums. Le sérialiseur mappe les valeurs d’enum sur leur type de backing et inversement. Trois composants mis à jour d’un coup, ce qui a rendu la migration des bases de code des pseudo-constantes enum vers les vrais enums PHP 8.1 étonnamment fluide.
Cache des voters de sécurité
La CacheableVoterInterface permet aux voters qui s’abstiennent toujours sur un attribut donné de le signaler au système de sécurité, qui peut alors les ignorer lors des vérifications suivantes. Pour les applications avec de nombreux voters, le gain sur les vérifications de permissions s’accumule vite. Petit changement, perceptible en pratique.
Messenger continue de mûrir
Le traitement par batch de Messenger (gérer plusieurs messages en une seule transaction au lieu d’un par un) est maintenant stable. Rate limiting par transport. Les dead letter queues bénéficient de meilleurs outils. Après des années en mode « expérimental », Messenger en 5.4 est enfin la fondation async sur laquelle on peut s’appuyer pour des charges sérieuses.
La Console a eu sa touche Tab
Symfony 5.4 embarque l’autocomplétion shell pour toutes les commandes. Appuyez sur Tab et le shell suggère les noms de commandes, les valeurs d’arguments et les valeurs d’options. Pour les commandes intégrées, ça fonctionne sans configuration. Pour les commandes personnalisées, ajoutez une méthode complete() :
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['json', 'xml', 'csv']);
}
}
Pas d’interface requise, juste la méthode et Symfony s’en charge. La communauté a aussi passé en revue toutes les commandes intégrées (debug:router, cache:pool:clear, secrets:remove, lint:twig, et une dizaine d’autres) pour ajouter les compléments avant la sortie.
Les routes peuvent être des alias maintenant
Le composant de routing supporte désormais les alias : une route peut pointer vers une autre. Le cas d’usage évident, c’est renommer une route sans casser tout ce qui génère encore des URLs avec l’ancien nom.
# config/routes.yaml
admin_dashboard:
path: /admin
# ancien nom conservé pendant la transition
dashboard:
alias: admin_dashboard
deprecated:
package: 'acme/my-bundle'
version: '2.3'
Générer une URL avec dashboard fonctionne toujours, mais déclenche un avertissement de dépréciation. Des chemins de renommage propres pour les bundles qui doivent maintenir des noms de routes publics tout en avançant.
Les exceptions sont mappées aux codes HTTP dans la config
Avant 5.4, mapper une classe d’exception à un code HTTP signifiait implémenter HttpExceptionInterface ou écrire un listener. Maintenant c’est juste une entrée YAML :
# config/packages/framework.yaml
framework:
exceptions:
App\Exception\PaymentRequiredException:
status_code: 402
log_level: warning
App\Exception\MaintenanceException:
status_code: 503
log_level: info
L’exception n’a rien à implémenter. Le framework lit la map, définit le code de statut, loggue au niveau configuré. Pratique pour les exceptions métier qui n’ont aucune raison de connaître HTTP.
Deux nouvelles contraintes de validation
5.4 ajoute Cidr et CssColor au composant Validator.
Cidr valide la notation réseau (adresse IP plus masque de sous-réseau) avec un contrôle sur la version IP acceptée et les bornes de la valeur du masque :
#[Assert\Cidr(version: 4, netmaskMin: 16, netmaskMax: 28)]
private string $allowedSubnet;
CssColor valide qu’une chaîne est une couleur CSS valide. Utile pour les éditeurs de thème, la config CMS, ou toute interface qui laisse les utilisateurs choisir des couleurs :
#[Assert\CssColor(
formats: Assert\CssColor::HEX_LONG,
message: "La couleur d'accentuation doit être une valeur hex à 6 chiffres.",
)]
private string $accentColor;
Attributs PHP imbriqués pour les contraintes de validation
Symfony 5.2 avait ajouté les contraintes de validation en attributs PHP, mais PHP 8.0 avait une limitation sur les attributs imbriqués. Les contraintes complexes comme All, Collection, ou AtLeastOneOf étaient impossibles à exprimer avec la syntaxe d’attribut seule. PHP 8.1 a levé cette restriction, et 5.4 en tire le meilleur parti :
use Symfony\Component\Validator\Constraints as Assert;
class CartItem
{
#[Assert\All([
new Assert\NotNull(),
new Assert\Range(min: 1),
])]
private array $quantities;
#[Assert\AtLeastOneOf(
constraints: [new Assert\Email(), new Assert\Url()],
message: 'Doit être un email ou une URL valide.',
)]
private string $contact;
}
Pas de docblocks d’annotations, pas de mapping XML. Des attributs PHP 8.1 purs, de bout en bout.
Injection de dépendances : trois choses à savoir
Les itérateurs taggués peuvent maintenant être injectés dans des service locators, qui n’acceptaient auparavant que des listes de services explicites. L’autowiring des types union fonctionne quand les deux côtés de l’union résolvent vers le même service, ce qui est courant avec les interfaces du serializer :
public function __construct(
private NormalizerInterface & DenormalizerInterface $serializer
) {}
#[SubscribedService] remplace l’introspection automatique que ServiceSubscriberTrait faisait implicitement. C’est maintenant un attribut explicite sur les méthodes, ce qui rend la dépendance visible sans aucune magie :
use Symfony\Contracts\Service\Attribute\SubscribedService;
class SomeService implements ServiceSubscriberInterface
{
#[SubscribedService]
private function router(): RouterInterface
{
return $this->container->get(__METHOD__);
}
}
Messenger : attributs, état des workers et reset de services
Les handlers Messenger peuvent abandonner MessageHandlerInterface en faveur de #[AsMessageHandler], qui permet aussi de lier un handler à un transport spécifique et de définir sa priorité, sans toucher au YAML :
#[AsMessageHandler(fromTransport: 'async', priority: 10)]
class ProcessOrderHandler
{
public function __invoke(ProcessOrder $message): void { /* ... */ }
}
L’état des workers est maintenant inspectable via WorkerMetadata dans les event listeners, utile quand vous avez des workers sur plusieurs transports et avez besoin de savoir lequel a déclenché un événement donné.
Les workers longue durée accumulent de l’état entre les messages : buffers de l’entity manager, caches en mémoire, connexions ouvertes. La nouvelle option reset_on_message prend en charge la réinitialisation de tous les services réinitialisables entre les messages :
framework:
messenger:
reset_on_message: true
Serializer : collecter les erreurs plutôt que lever
Désérialiser du JSON externe dans un DTO typé levait une exception dès la première discordance de type. L’option COLLECT_DENORMALIZATION_ERRORS change ça : toutes les erreurs de type sont collectées dans une PartialDenormalizationException, pour que vous puissiez retourner un 400 propre avec la liste complète des problèmes par champ :
try {
$dto = $serializer->deserialize($request->getContent(), OrderDto::class, 'json', [
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
]);
} catch (PartialDenormalizationException $e) {
return $this->json(
array_map(fn($err) => ['path' => $err->getPath(), 'expected' => $err->getExpectedTypes()], $e->getErrors()),
400
);
}
Le contexte par défaut du serializer peut aussi être défini globalement en YAML, pour ne plus passer les mêmes options à chaque appel.
Négociation de langue intégrée
Deux nouvelles options du framework gèrent l’en-tête Accept-Language sans listeners personnalisés :
framework:
enabled_locales: ['en', 'fr', 'de']
set_locale_from_accept_language: true
set_content_language_from_locale: true
Avec ça en place, Symfony lit la langue préférée du navigateur, choisit la meilleure correspondance parmi enabled_locales, définit la locale de la requête, et ajoute un en-tête Content-Language à la réponse. L’attribut de route {_locale} a toujours la priorité quand il est présent.
Traduction : extraction, pas mise à jour
La commande translation:update est renommée en translation:extract. L’ancien nom reste comme déprécié. La distinction compte : la commande n’écrit jamais dans une base de données, elle extrait les chaînes traduisibles des fichiers source. Le nouveau nom dit enfin ce qu’elle fait.
lint:xliff gagne aussi une option --format=github qui sort les erreurs en annotations GitHub Actions, pour que les échecs de lint de traduction apparaissent en commentaires de revue de PR plutôt que de se noyer dans les logs.
Raccourcis du contrôleur élagués
Trois raccourcis d’AbstractController sont dépréciés : getDoctrine(), dispatchMessage(), et la méthode générique get() pour récupérer des services arbitraires du container. La direction, c’est l’injection par constructeur explicite. Pour getDoctrine() en particulier :
// avant
$em = $this->getDoctrine()->getManager();
// après — injecter directement
public function __construct(private EntityManagerInterface $em) {}
Request::get() est aussi déprécié. Il cherchait dans les attributs de route, la query string et le corps POST dans un ordre non documenté, ce qui était une excellente façon d’obtenir des résultats surprenants. Utilisez $request->query->get(), $request->request->get(), ou $request->attributes->get() et soyez explicite sur la provenance de la valeur.
La classe utilitaire Path
Le composant Filesystem reçoit une classe Path portée depuis webmozart/path-util. Elle gère les cas tordus que dirname() et realpath() ratent :
use Symfony\Component\Filesystem\Path;
Path::canonicalize('../config/../config/services.yaml'); // '../config/services.yaml'
Path::getDirectory('C:/'); // 'C:/' (dirname() retourne '.')
Path::getLongestCommonBasePath([
'/var/www/project/src/Controller/FooController.php',
'/var/www/project/src/Controller/BarController.php',
'/var/www/project/src/Entity/User.php',
]);
// '/var/www/project/src'
Utile dès que votre code manipule des chemins qui traversent les frontières des OS ou qui contiennent des segments relatifs.
Les petites choses qui s’accumulent
debug:dotenv montre quels fichiers .env ont été chargés et quelle valeur chaque variable résout. La première chose qu’on cherche quand un comportement spécifique à un environnement déraille.
Le composant String ajoute trimPrefix() et trimSuffix() pour retirer des préfixes ou suffixes connus sans écrire un calcul de substr :
u('file-image-0001.png')->trimPrefix('file-'); // 'image-0001.png'
u('template.html.twig')->trimSuffix('.twig'); // 'template.html'
DomCrawler reçoit innerText(), qui retourne uniquement le texte direct d’un nœud, à l’exclusion des éléments enfants. text() retourne tout y compris le texte imbriqué ; innerText() retourne uniquement le contenu propre du nœud. Petite différence, mais ça compte quand on fait du scraping.
Le composant RateLimiter étend son support des intervalles avec perMonth() et perYear(), pour les applications qui ont besoin de limiter des événements sur des fenêtres plus longues : envois de newsletters, remises à zéro de quotas API, limites de forfaits annuels.
Le composant Finder respecte maintenant les fichiers .gitignore dans tous les sous-répertoires quand vous appelez ignoreVCSIgnored(true), pas seulement à la racine. Les règles des répertoires enfants supplantent celles des parents, exactement comme git lui-même.
La fenêtre LTS
5.4 reçoit des corrections de bugs jusqu’en novembre 2024 et des correctifs de sécurité jusqu’en novembre 2025. La migration de 5.4 vers 6.4 (la prochaine LTS) est intentionnellement fluide : corrigez les avertissements de dépréciation de 5.4, et le saut vers 6.x devient mécanique.
La couche de dépréciation en 5.4 pointe vers tout ce que 6.0 supprime : les derniers morceaux de l’ancien système de sécurité, ContainerAwareTrait, et quelques patterns legacy de formulaires et de serializer.