Symfony 4.4 et 5.0 sont tous les deux sortis le 21 novembre 2019. La 4.4 est la LTS : même ensemble de fonctionnalités que la 5.0, couche de dépréciation intégrée, et une longue fenêtre de support pour les équipes qui ne peuvent pas suivre chaque release.

La fonctionnalité qui mérite d’être mise en avant est arrivée en 4.2 et a mûri tout au long des 4.3 et 4.4 : HttpClient.

HttpClient

Les options HTTP natives de PHP (file_get_contents avec des contextes de flux, cURL, Guzzle) ont chacune leur propre modèle, leurs propres bizarreries et leur propre coût d’abstraction. Symfony 4.2 a introduit HttpClient, un client HTTP first-party avec une seule API pour plusieurs transports.

L’interface est claire :

$response = $client->request('GET', 'https://api.example.com/users');
$users = $response->toArray();

L’implémentation est asynchrone par défaut. Les réponses sont paresseuses : la requête réseau n’a pas lieu tant qu’on ne lit pas réellement la réponse. Plusieurs requêtes peuvent être initiées et résolues au fil de l’arrivée des données, sans threads ni callbacks.

Le transport mock intégré (MockHttpClient) rend les tests d’appels HTTP indolores sans avoir à démarrer des serveurs ou patcher des fonctions globales.

Mailer

Également stabilisé en 4.4 : le composant Mailer, qui remplace SwiftMailerBundle comme solution d’email recommandée. Le transport se configure via DSN :

MAILER_DSN=smtp://user:pass@smtp.example.com:587

L’approche DSN signifie que changer de fournisseur (Mailgun, Postmark, SES, SMTP local) est un changement de config, pas un changement de code. Les tests d’emails utilisent un spooler par défaut dans les environnements hors production.

Messenger se mâture

Le composant Messenger a atterri en 3.4 à titre expérimental. En 4.4, il est stable et éprouvé en production : gestion de messages asynchrones avec logique de retry, transport d’échec, et adaptateurs pour AMQP, Redis, Doctrine, et les transports in-process.

Le pattern qu’il permet (traiter une requête de façon synchrone, dispatcher du travail de façon asynchrone, réessayer en cas d’échec) remplace toute une classe de setups Gearman/RabbitMQ qui nécessitaient des bibliothèques tierces et une configuration conséquente.

La fenêtre LTS

La 4.4 est supportée pour les bugs jusqu’en novembre 2022 et pour les correctifs de sécurité jusqu’en novembre 2023. Si vous êtes sur la 4.x et recherchez la stabilité, c’est un endroit confortable où rester. Les avertissements de dépréciation qu’elle introduit pointent directement vers ce que la 5.0 exigera.

Le composant Messenger, de l’expérimental à la production

Messenger est arrivé en 4.1 comme une expérience. Le concept était simple : dispatcher un objet message vers un bus, le traiter immédiatement ou le router vers un transport pour un traitement asynchrone. En 4.3 et 4.4, l’expérience était devenue de l’infrastructure.

La release 4.3 a ajouté un transport d’échec dédié. Quand un message échoue après toutes les tentatives de retry, il va quelque part de récupérable plutôt que de simplement disparaître :

framework:
    messenger:
        failure_transport: failed
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
            failed: 'doctrine://default?queue_name=failed'
        routing:
            App\Message\SendEmail: async

Les messages qui atterrissent dans failed peuvent être inspectés et retentés manuellement. Avant ça, les messages en échec étaient une entrée de log et un mal de tête. Après, c’est une file qu’on peut vraiment travailler.

Le dispatching d’événements, avec les objets en première place

Depuis le début, le système d’événements de Symfony utilisait des noms d’événements en chaîne comme identifiant principal. On définissait OrderEvents::NEW_ORDER = 'order.new_order', on écoutait cette chaîne, et on passait l’objet événement comme paramètre secondaire.

La 4.3 a inversé ça. L’objet événement passe en premier, et le nom de l’événement devient optionnel :

// Avant
$dispatcher->dispatch(OrderEvents::NEW_ORDER, $event);

// 4.3+
$dispatcher->dispatch($event);

Omettez le nom et Symfony utilise le nom de classe comme identifiant. Les listeners et subscribers peuvent maintenant référencer la classe directement :

public static function getSubscribedEvents(): array
{
    return [
        OrderPlacedEvent::class => 'onOrderPlaced',
    ];
}

Les événements HttpKernel ont été renommés en conséquence : GetResponseEvent est devenu RequestEvent, FilterResponseEvent est devenu ResponseEvent. Les anciens noms sont restés comme alias pendant toute la 4.x.

VarDumper obtient un serveur

Un dump() dans un contrôleur qui retourne du JSON, et votre sortie de debug se retrouve injectée directement dans le corps de la réponse. Pour le développement d’API, c’est suffisamment agaçant pour que les gens désactivent le dumping complètement.

La 4.1 a ajouté un serveur VarDumper qui capture les dumps séparément :

bin/console server:dump

Configurez la destination du dump dans config/packages/dev/debug.yaml :

debug:
    dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

Maintenant, dump() dans votre contrôleur API envoie les données vers la console du serveur au lieu de polluer la réponse. Le serveur affiche le dump avec le fichier source, la requête HTTP qui l’a déclenché, et le timestamp.

VarExporter, pour quand var_export() vous déçoit

var_export() a deux problèmes : il ignore la sémantique de sérialisation et sa sortie n’est pas conforme à PSR-2. Le composant VarExporter de la 4.2 corrige les deux.

$exported = VarExporter::export([123, ['abc', true]]);
// Retourne :
// [
//     123,
//     [
//         'abc',
//         true,
//     ],
// ]

Plus important encore, il gère correctement les objets implémentant Serializable, __sleep, et __wakeup. Là où var_export() abandonne silencieusement les méthodes de sérialisation et exporte les propriétés brutes, VarExporter produit du code qui appelle les mêmes hooks qu’unserialize() utiliserait. Le cas d’usage pratique est le préchauffage du cache : générer des fichiers PHP que OPcache peut charger sans ré-exécuter des calculs coûteux.

Des mots de passe vérifiés contre les bases de données de violations

La contrainte NotCompromisedPassword est arrivée en 4.3. Elle vérifie les mots de passe soumis contre la base de données de violations d’haveibeenpwned.com sans envoyer le vrai mot de passe nulle part.

use Symfony\Component\Validator\Constraints as Assert;

class User
{
    #[Assert\NotCompromisedPassword]
    public string $plainPassword;
}

L’implémentation utilise la k-anonymité : on hash le mot de passe en SHA-1, on envoie seulement les cinq premiers caractères à l’API, on récupère tous les hashs correspondants, on vérifie localement. Le mot de passe ne quitte jamais votre serveur. Pour les formulaires d’inscription, ajouter cette contrainte c’est une ligne et un signal de sécurité vraiment utile.

Workflow obtient du contexte

Le composant Workflow existait avant la 4.x, mais la 4.3 a ajouté la propagation de contexte : la possibilité de passer des données arbitraires à travers une transition et d’y accéder dans les listeners.

$workflow->apply($article, 'publish', [
    'user' => $user->getUsername(),
    'reason' => $request->request->get('reason'),
]);

Le contexte arrive dans TransitionEvent et est stocké aux côtés du marquage. Pour les pistes d’audit, c’est la différence entre savoir qu’une transition s’est produite et savoir qui l’a déclenchée et pourquoi. On peut aussi injecter du contexte depuis un subscriber sans toucher à chaque appel apply(), ce qui est pratique pour les préoccupations transversales comme les timestamps ou l’utilisateur courant.

L’autowiring est devenu plus intelligent

La 4.2 a ajouté la liaison par type et par nom simultanément. Avant, on pouvait lier par type (LoggerInterface) ou par nom ($logger), mais pas les deux en même temps. Ça posait problème quand un service a besoin de deux implémentations différentes de la même interface :

services:
    _defaults:
        bind:
            Psr\Log\LoggerInterface $orderLogger: '@monolog.logger.orders'
            Psr\Log\LoggerInterface $paymentLogger: '@monolog.logger.payments'
class OrderService
{
    public function __construct(
        private LoggerInterface $orderLogger,   // gets monolog.logger.orders
        private LoggerInterface $paymentLogger, // gets monolog.logger.payments
    ) {}
}

La correspondance exige que le type et le nom de l’argument s’alignent, donc pas de risque d’injecter accidentellement le mauvais logger.

ErrorHandler remplace le composant Debug

Le composant Debug, inchangé depuis 2013, avait une dépendance maladroite sur TwigBundle même pour les apps API-only. Toute exception non attrapée dans une API JSON rendait une page d’erreur HTML à moins d’écrire des listeners d’exception personnalisés.

La 4.4 extrait ça dans un composant ErrorHandler dédié. Pour les requêtes non-HTML, les réponses d’erreur suivent désormais RFC 7807 nativement :

{
    "title": "Not Found",
    "status": 404,
    "detail": "Sorry, the page you are looking for could not be found"
}

Pas besoin de Twig. Le format suit l’en-tête Accept : JSON pour les requêtes JSON, XML pour les requêtes XML. Pour personnaliser davantage, on fournit un normalizer via le composant Serializer plutôt qu’un template Twig.

Le préchargement PHP 7.4, câblé automatiquement

PHP 7.4 a introduit le préchargement OPcache : charger des fichiers en mémoire partagée avant l’arrivée de toute requête, pour qu’ils soient disponibles sous forme d’opcodes compilés dès la toute première requête. Le gain pratique est de 30 à 50 % de temps de réponse en moins sans changer une ligne de code.

Le bémol c’est la configuration : il faut spécifier exactement quels fichiers précharger dans php.ini. Symfony 4.4 génère ce fichier automatiquement dans le répertoire de cache :

; php.ini
opcache.preload=/path/to/project/var/cache/prod/App_KernelProdContainer.preload.php
opcache.preload_user=www-data

Lancez cache:warmup en production et pointez OPcache vers le fichier généré. Symfony précharge le container, les routes compilées et les templates Twig : les fichiers lus à chaque requête et qui ne changent jamais entre les déploiements.

Console : codes de retour et NO_COLOR

Deux petites choses en 4.4 qui auraient honnêtement dû exister plus tôt. Les commandes qui ne retournent pas d’entier depuis execute() déclenchent maintenant un avertissement de dépréciation. En 5.0, le type de retour devient obligatoire. Retourner 0 pour le succès, non-zéro pour l’échec : comportement Unix standard, et ça rend l’intégration avec les superviseurs de processus et les pipelines CI sans ambiguïté.

protected function execute(InputInterface $input, OutputInterface $output): int
{
    // ...
    return Command::SUCCESS; // = 0
}

Le deuxième point : le support de la variable d’environnement NO_COLOR, suivant la convention de no-color.org. Activez-la et toutes les commandes console de Symfony abandonnent les codes d’échappement ANSI quelle que soit la capacité déclarée par le terminal. Utile pour les environnements CI qui capturent la sortie en texte et qui s’étranglent sur les codes couleur intégrés dans les logs.