Symfony 7.4 est sorti en novembre 2025, aux côtés de 8.0. C’est la dernière LTS de la ligne 7.x : PHP 8.2 minimum, trois ans de corrections de bugs, quatre de sécurité. Pour les équipes qui ne peuvent pas ou ne veulent pas suivre l’exigence PHP 8.4 de 8.0, 7.4 est l’endroit où atterrir.

La signature de messages dans Messenger

La sécurité des transports dans Messenger a toujours été le problème de l’application à résoudre. 7.4 ajoute la signature de messages : un mécanisme basé sur des stamps qui signe les messages dispatchés et valide les signatures à la réception.

Le cas d’usage cible est les scénarios multi-tenant ou de transport externe où on a besoin d’une preuve cryptographique qu’un message n’a pas été altéré ni injecté depuis l’extérieur. La configuration vit au niveau du transport :

framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    signing_key: '%env(MESSENGER_SIGNING_KEY)%'

Configuration en tableaux PHP

Les formats de configuration de Symfony ont toujours été YAML (défaut), XML et PHP. Le format PHP existait mais était maladroit : un DSL builder fluent qui nécessitait du chaînage de méthodes et ne donnait rien d’utile à l’IDE.

7.4 remplace le format fluent par des tableaux PHP standard. Les IDEs peuvent maintenant vraiment l’analyser, config/reference.php est auto-généré comme référence avec annotations de types, et le résultat ressemble à des données plutôt qu’à du code :

return static function (FrameworkConfig $framework): void {
    $framework->router()->strictRequirements(null);
    $framework->session()->enabled(true);
};

Le format fluent est déprécié. Les tableaux sont l’avenir, et honnêtement c’est un meilleur format.

Améliorations OIDC

#[IsSignatureValid] valide les URLs signées directement dans les contrôleurs, supprimant le boilerplate de la validation manuelle. OpenID Connect supporte maintenant plusieurs endpoints de découverte, et une nouvelle commande security:oidc-token:generate rend le dev et les tests beaucoup moins pénibles.

La fenêtre de support

7.4 LTS : bugs jusqu’en novembre 2028, correctifs de sécurité jusqu’en novembre 2029. Le chemin vers 8.4 LTS (la prochaine cible long terme) passe par les notices de dépréciation de 7.4 et la mise à jour PHP 8.4. Corriger les dépréciations maintenant et le saut vers 8.x sera beaucoup moins douloureux.

Les attributs deviennent plus précis

#[CurrentUser] accepte maintenant les types union, ce qui compte en pratique quand une route est accessible par plus d’une classe utilisateur :

public function index(#[CurrentUser] AdminUser|Customer $user): Response

#[Route] accepte un tableau pour l’option env, donc une route de debug active seulement en dev et test n’a plus besoin de deux définitions séparées. #[AsDecorator] est maintenant répétable, ce qui signifie qu’une classe peut décorer plusieurs services à la fois. Les signatures de méthode #[AsEventListener] acceptent les types d’événements union. #[IsGranted] reçoit une option methods pour limiter une vérification d’autorisation à des verbes HTTP spécifiques sans dupliquer la route.

La classe Request arrête d’en faire trop

Request::get() est dépréciée, et franchement bonne débarrassance. La méthode cherchait dans les attributs de route, puis les paramètres de query, puis le corps de la requête, dans cet ordre, retournant silencieusement ce qu’elle trouvait en premier. Cette ambiguïté causait de vrais bugs. Elle est supprimée dans 8.0 ; dans 7.4 elle fonctionne encore mais déclenche une dépréciation. Les remplacements sont explicites : $request->attributes->get(), $request->query->get(), $request->request->get().

Le parsing du corps pour les requêtes PUT, PATCH, DELETE et QUERY arrive en même temps. Auparavant Symfony ne parsait application/x-www-form-urlencoded et multipart/form-data que pour POST. Ces mêmes types de contenu sont maintenant parsés pour les autres méthodes accessibles en écriture aussi, ce qui tue un contournement REST API courant.

La surcharge de méthode HTTP pour GET, HEAD, CONNECT et TRACE est dépréciée. Surcharger une méthode sûre avec un header était de toute façon toujours sémantiquement cassé. On peut maintenant autoriser explicitement seulement les méthodes qui ont du sens pour son application :

Request::setAllowedHttpMethodOverride(['PUT', 'PATCH', 'DELETE']);

Les Workflows acceptent les BackedEnums

Les places et transitions de Workflow peuvent maintenant être définies avec des backed enums PHP, à la fois en YAML (via le tag !php/enum) et en config PHP. Le marking store fonctionne avec les valeurs d’enum directement, donc le modèle de domaine et la définition du workflow utilisent enfin les mêmes types :

framework:
    workflows:
        blog_publishing:
            initial_marking: !php/enum App\Status\PostStatus::Draft
            places: !php/enum App\Status\PostStatus
            transitions:
                publish:
                    from: !php/enum App\Status\PostStatus::Review
                    to: !php/enum App\Status\PostStatus::Published

Étendre la validation et la sérialisation pour les classes tierces

Besoin d’ajouter des métadonnées de validation ou de sérialisation à une classe d’un bundle qu’on ne possède pas ? 7.4 a #[ExtendsValidationFor] et #[ExtendsSerializationFor] pour ça. On écrit une classe compagnon avec ses annotations supplémentaires, on pointe l’attribut vers la classe cible, et Symfony fusionne les métadonnées à la compilation du conteneur :

#[ExtendsValidationFor(UserRegistration::class)]
abstract class UserRegistrationValidation
{
    #[Assert\NotBlank(groups: ['my_app'])]
    #[Assert\Length(min: 3, groups: ['my_app'])]
    public string $name = '';

    #[Assert\Email(groups: ['my_app'])]
    public string $email = '';
}

Symfony vérifie à la compilation que les propriétés déclarées existent réellement sur la classe cible. Un renommage ne cassera pas silencieusement la validation.

DX : ce qui ne fait pas la une mais compte

Le helper Question dans Console accepte un timeout. Demander à l’utilisateur de confirmer quelque chose, et s’il ne répond pas en N secondes, la réponse par défaut s’applique. Très pratique dans les scripts de déploiement qui ne peuvent pas se permettre d’attendre éternellement un humain.

messenger:consume reçoit --exclude-receivers. Combiné avec --all, il permet de consommer depuis tous les transports sauf des spécifiques :

bin/console messenger:consume --all --exclude-receivers=low_priority

Le mode worker FrankenPHP est maintenant auto-détecté. Si le processus tourne dans FrankenPHP, Symfony bascule en mode worker automatiquement. Pas de package supplémentaire nécessaire.

La commande debug:router cache les colonnes Scheme et Host quand toutes les routes utilisent ANY, ce qui supprime beaucoup de bruit de la sortie par défaut. Les méthodes HTTP sont maintenant aussi colorées.

Les tests fonctionnels reçoivent $client->getSession() avant la première requête. Auparavant il fallait faire au moins une requête pour accéder à la session, ce qui était agaçant. Maintenant on peut pré-remplir les tokens CSRF ou les flags A/B en amont :

$session = $client->getSession();
$session->set('_csrf/checkout', 'test-token');
$session->save();

Lock : store DynamoDB

DynamoDbStore arrive comme nouveau backend de Lock. Utile dans les déploiements AWS-natifs où Redis n’est pas dans la stack, et ça fonctionne exactement comme n’importe quel autre store :

$store = new DynamoDbStore('dynamodb://default/locks');
$factory = new LockFactory($store);

Bridge Doctrine : types day point et time point

Deux nouveaux types de colonnes Doctrine : day_point stocke une valeur date uniquement (sans composant heure) et time_point stocke une valeur heure uniquement, tous deux mappant vers DatePoint. Bien quand le domaine sépare genuinement la date de l’heure :

#[ORM\Column(type: 'day_point')]
public DatePoint $birthDate;

#[ORM\Column(type: 'time_point')]
public DatePoint $openingTime;

Routing : paramètres de query explicites

La clé _query dans la génération d’URL permet de définir les paramètres de query explicitement, séparément des paramètres de route. Ça compte quand un paramètre de route et un paramètre de query partagent le même nom :

$url = $urlGenerator->generate('report', [
    'site' => 'fr',
    '_query' => ['site' => 'us'],
]);
// /report/fr?site=us

HttpHeaderParser parse les en-têtes de réponse Link en objets structurés. Avant ça, parser des en-têtes Link depuis des réponses d’API nécessitait soit d’importer une bibliothèque tierce, soit d’écrire des regex. Le cas d’usage : les APIs HTTP qui annoncent des ressources liées ou la pagination via des en-têtes Link, comme le fait l’API GitHub.

Le parsing HTML5 est plus rapide sur PHP 8.4

DomCrawler et HtmlSanitizer basculent vers le parser HTML5 natif de PHP 8.4 quand il est disponible. Pas de changements de code nécessaires de votre côté. Le parser natif est plus rapide et plus conforme à la spec que le fallback précédent. Sur PHP 8.2 ou 8.3, rien ne change.

Translation : StaticMessage

StaticMessage implémente TranslatableInterface mais ne traduit intentionnellement pas. Elle passe la string inchangée quelle que soit la locale. Le cas d’usage : les réponses d’API qui doivent rester dans une langue fixe quelle que soit la locale de l’utilisateur, ou les entrées de log d’audit où on doit préserver le texte original tel quel.