Symfony 5.0 est sorti le 21 novembre 2019, le même jour que la 4.4. Là où la 4.4 mise sur la stabilité et une longue fenêtre de support, la 5.0 ouvre un nouveau chapitre : plus de code déprécié, PHP 7.2.5 minimum, et quelques nouveaux composants qui comblent enfin des lacunes accumulées depuis des années.

Le composant String

La gestion des chaînes en PHP est notoirement éparpillée : des fonctions avec préfixe par-ci (str_), avec suffixe par-là (strpos), un support d’encodage incohérent, et rien d’orienté objet en vue. Le composant String enveloppe tout ça dans une API fluide orientée objet avec support Unicode :

use Symfony\Component\String\UnicodeString;

$str = new UnicodeString('  Hello World  ');
echo $str->trim()->lower()->replace(' ', '-'); // hello-world

L’ajout pratique, c’est le Slugger, un générateur de slug locale-aware qui gère vraiment correctement les caractères accentués :

$slug = $slugger->slug('L\'été à Montréal'); // l-ete-a-montreal

Avant, il fallait intégrer une bibliothèque tierce ou en écrire une soi-même. Maintenant ça ship avec FrameworkBundle, disponible par défaut.

Notifier

Le courrier électronique est géré par Mailer. SMS, notifications push, messages de chat : pas de solution first-party, jusqu’à maintenant. Le composant Notifier en ajoute une : une interface unifiée sur des dizaines de canaux et fournisseurs.

La même notification peut atterrir sur Slack, déclencher un SMS via Twilio, ou finir comme notification push, tout configuré via des DSN. Ajouter un nouveau canal, c’est un changement de config, pas un changement de code.

Le coffre-fort de secrets

Stocker des secrets dans des fichiers .env fonctionne, mais les valeurs sont en clair, les environnements partagés sont une galère, et il n’y a aucun moyen natif de chiffrer quoi que ce soit au repos.

Symfony 5.0 ajoute une famille de commandes secrets: et un mécanisme de coffre-fort. Les secrets sont chiffrés avec une paire de clés stockée hors du dépôt. Les fichiers chiffrés sont commités ; la clé de déchiffrement ne l’est pas. En production, la clé arrive comme variable d’environnement ou est injectée depuis un gestionnaire de secrets.

php bin/console secrets:set DATABASE_PASSWORD
php bin/console secrets:decrypt-to-local --force

Pas une solution de gestion de secrets à part entière, mais un vrai pas en avant par rapport à un fichier .env en clair qui traîne non chiffré dans le dépôt.

Mailer reçoit une couche de notification

Le composant Mailer est arrivé en 4.4. Ce que la 5.0 ajoute par-dessus, c’est la NotificationEmail : un email pré-stylisé et responsive construit sur Foundation for Emails, avec une API explicite pour les niveaux d’importance et les boutons d’appel à l’action :

use Symfony\Bridge\Twig\Mime\NotificationEmail;

$email = (new NotificationEmail())
    ->from('alerts@example.com')
    ->to('admin@example.com')
    ->subject('Disk usage critical')
    ->markdown('The disk on **prod-01** is at 94%. Check it now.')
    ->action('Open dashboard', 'https://example.com/servers')
    ->importance(NotificationEmail::IMPORTANCE_URGENT);

Pas de template à écrire, pas de CSS inline à dompter. Pour les alertes transactionnelles, les notifications de facturation et les emails système, ça couvre 80 % de ce dont on a besoin sans toucher à quoi que ce soit.

Les firewalls paresseux et le problème de cache

Chaque firewall stateful dans Symfony charge l’utilisateur depuis la session à chaque requête, que l’action en ait besoin ou non. Ce qui signifie que toute réponse est non-cacheable par défaut, même pour des pages qui ne touchent jamais à $this->getUser().

La 5.0 ajoute le mode lazy pour les firewalls, qui diffère l’accès à la session jusqu’à ce que le code appelle réellement is_granted() ou accède au token utilisateur :

# config/packages/security.yaml
security:
    firewalls:
        main:
            pattern: ^/
            anonymous: lazy

Les pages qui n’ont pas besoin de l’utilisateur redeviennent cacheables. Les nouveaux projets obtiennent ça par défaut via la recette Flex ; les existants ont besoin d’un changement de config en une ligne.

Les migrations de mots de passe sans grand soir

Migrer une app en production de bcrypt vers argon2id impliquait jusqu’ici de forcer une réinitialisation du mot de passe pour chaque utilisateur. Le PasswordUpgraderInterface rend ça progressif : à la connexion, Symfony vérifie si le hash stocké correspond à l’algorithme courant. Sinon, il le re-hash sur place et appelle votre upgrader pour le sauvegarder :

// src/Repository/UserRepository.php
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    {
        $user->setPassword($newHashedPassword);
        $this->getEntityManager()->flush();
    }
}

Associez ça à algorithm: auto dans la config de l’encodeur, et les anciens hashs migrent silencieusement à mesure que les utilisateurs se connectent. Pas de script de migration, pas de downtime, pas de friction pour l’utilisateur.

ErrorHandler remplace Debug

Le composant Debug est parti. Son remplaçant, ErrorHandler, fait le même travail (convertir les erreurs PHP en exceptions, afficher de belles pages d’erreur) mais sans nécessiter Twig. Pour les apps API qui ne rendent jamais de HTML, ça compte : ErrorHandler génère les erreurs dans le format de la requête (JSON, XML, texte) en suivant RFC 7807 :

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

La config de routing passe de TwigBundle à FrameworkBundle, et c’est la seule étape de migration pour la plupart des projets. Une ligne, c’est fait.

Les listeners d’événements, enfin moins verbeux

Enregistrer un listener d’événement kernel impliquait auparavant de nommer explicitement l’événement dans le tag de service. Symfony 5.0 l’infère depuis la signature de méthode :

// Pas de configuration de tag au-delà de kernel.event_listener
final class SecurityListener
{
    public function onKernelRequest(RequestEvent $event): void
    {
        // Symfony lit le type hint et détermine l'événement
    }
}
# config/services.yaml
App\EventListener\SecurityListener:
    tags: [kernel.event_listener]

Utilisez __invoke() et ça fonctionne de la même façon. Enregistrez en masse tout un répertoire de listeners avec un seul bloc resource, et Symfony détermine quel événement chacun gère.

HttpClient grandit

Le composant HttpClient est arrivé en 4.4 comme stable. La 5.0 ajoute quelques choses utiles par-dessus :

L’authentification NTLM pour les environnements d’entreprise, le buffering conditionnel via un callback (bufferiser les grandes réponses seulement quand le content-type correspond), une option max_duration qui plafonne le temps total de requête indépendamment des conditions réseau, et toStream() pour transformer n’importe quelle réponse en un flux PHP standard pour le code qui attend du fread() :

$response = $client->request('GET', 'https://api.example.com/large-export', [
    'max_duration' => 30.0,
    'buffer' => fn(array $headers): bool => str_contains($headers['content-type'][0] ?? '', 'json'),
]);

// Le streamer plutôt que de tout charger en mémoire
$stream = $response->toStream();

Le client a aussi obtenu une interopérabilité complète avec PSR-18 et HTTPlug v1/v2, donc toute bibliothèque qui dépend de ces abstractions fonctionne directement avec lui.

Ce que la 5.0 supprime

La 5.0 abandonne tout ce qui était déprécié en 4.4. Les plus notables :

  • WebServerBundle (utilisez symfony server:start depuis l’outil CLI à la place)
  • L’AnonymousToken de l’ancien système de sécurité (remplacé par NullToken)
  • Les anciens noms d’événements de formulaire
  • Le ClassLoader interne de Symfony
  • Le composant Debug (remplacé par ErrorHandler)

Si vous avez fait tourner votre app 4.4 avec les notices de dépréciation actives et corrigé les avertissements, la mise à niveau vers la 5.0 ne nécessite aucun changement de code.