Symfony 6.0 est sorti le 29 novembre 2021. La caractéristique définissante : PHP 8.1 est le minimum. Pas supporté, requis. L’équipe de releases a attendu que PHP 8.1 sorte, puis a coupé Symfony 6.0 le lendemain.

Ce n’est pas juste un bump de version. C’est un engagement à construire contre le langage actuel plutôt que le plancher historique.

Le système de sécurité, enfin reconstruit

Le composant de sécurité Symfony a deux systèmes. L’ancien (AnonymousToken, GuardAuthenticatorInterface, un enchevêtrement d’interfaces qui vous faisaient implémenter des méthodes dont vous n’aviez pas besoin) avait été déprécié. 6.0 le supprime entièrement.

Le nouveau système de sécurité (security.enable_authenticator_manager: true en 5.x) est maintenant le seul système. C’est plus propre : une seule interface à implémenter, séparation claire entre authentification et autorisation, vérification des credentials basée sur des passeports. La migration depuis les anciens guard authenticators n’est pas indolore, mais la destination est beaucoup moins confuse.

La classe Path du Filesystem

Travailler avec des chemins de fichiers en PHP est fondamentalement un problème de manipulation de strings. __DIR__, concaténation, realpath(), séparateurs spécifiques à la plateforme : la bibliothèque standard donne des primitives mais pas vraiment un modèle.

La nouvelle classe Path gère ça :

use Symfony\Component\Filesystem\Path;

Path::join('/var/www', 'html', '../uploads'); // /var/www/uploads
Path::makeRelative('/var/www/html', '/var/www'); // html
Path::isAbsolute('./relative/path'); // false

Multiplateforme, sans effets de bord, sans accès au filesystem nécessaire. Aussi dans 6.0 : support des patterns .gitignore imbriqués dans Finder.

Les enums dans le système de formulaires

En s’appuyant sur les fondations posées par 5.4, 6.0 pousse le support des enums plus loin. Les valeurs BackedEnum font des allers-retours à travers les formulaires et le sérialiseur sans transformateurs personnalisés. Le composant de formulaire comprend les cases d’enum comme options de choix nativement.

Ce que 6.0 supprime

La liste des suppressions est extensive : l’ancien système de sécurité, le composant Templating, le support des annotations PHP (remplacées par les attributs natifs), le support du cache Doctrine, ContainerAwareTrait. Six années de marqueurs @deprecated accumulés, finalement nettoyés.

Les applications qui avaient pris au sérieux les warnings de dépréciation de 5.4 avaient un chemin de migration propre. Celles qui ne l’avaient pas fait avaient du travail à faire.

La complétion automatique était toujours le manque

Le composant Console a reçu l’autocomplétion shell, et c’est proprement intégré : définir une méthode complete() sur sa commande, et Tab dans Bash suggère des valeurs valides pour les options et arguments.

class DeployCommand extends Command
{
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestOptionValuesFor('env')) {
            $suggestions->suggestValues(['prod', 'staging', 'dev']);
        }
    }
}

Toutes les commandes Symfony intégrées ont reçu la complétion aussi : debug:router, cache:pool:clear, lint:yaml, et une quinzaine d’autres. Exécuter bin/console completion bash >> ~/.bashrc et c’est terminé.

Messenger, maintenant avec attributs et traitement par lots

L’attribut #[AsMessageHandler] remplace l’ancienne MessageHandlerInterface. Moins de boilerplate, et on peut maintenant configurer l’affinité de transport et la priorité directement sur l’attribut :

#[AsMessageHandler(fromTransport: 'async', priority: 10)]
class SendWelcomeEmailHandler
{
    public function __invoke(UserRegistered $message): void { ... }
}

L’autre ajout significatif : BatchHandlerInterface. Quand on insère un millier de lignes, traiter les messages un par un est du gaspillage. Les batch handlers collectent les messages et les traitent en groupes. La taille de lot par défaut est 10, contrôlée par BatchHandlerTrait::shouldFlush(). L’Acknowledger gère le succès et l’échec individuels dans le lot.

reset_on_message: true dans la config Messenger réinitialise les services du conteneur entre les messages. Auparavant, un buffer Monolog pouvait se remplir à travers la gestion des messages et personne ne s’en rendait compte avant la production. Ça évite cette catégorie de bugs de stateful sans nécessiter de nettoyage manuel.

Le conteneur DI devient plus expressif

Trois changements qui comptent en pratique.

Les types union et intersection s’autowire maintenant. PHP 8.1 a ajouté les types intersection, et Symfony 6.0 les câble :

public function __construct(
    private NormalizerInterface&DenormalizerInterface $serializer
) {}

Ça fonctionne tant que les deux interfaces pointent vers le même service via les alias d’autowiring.

TaggedIterator et TaggedLocator ont reçu les options defaultPriorityMethod et defaultIndexMethod. On n’a plus besoin de YAML pour exprimer l’ordonnancement ou l’indexation pour les services taggués :

public function __construct(
    #[TaggedIterator(tag: 'app.handler', defaultPriorityMethod: 'getPriority')]
    private iterable $handlers,
) {}

SubscribedService (l’attribut qui remplace la magie implicite de ServiceSubscriberTrait) rend l’accès paresseux aux services explicite et typable :

#[SubscribedService]
private function mailer(): MailerInterface
{
    return $this->container->get(__METHOD__);
}

La validation reçoit trois nouveaux outils

CssColor valide les valeurs de couleurs CSS dans les formats voulus : hex, RGB, HSL, couleurs nommées, ou n’importe quel mélange. Utile pour les champs de configuration de thème où on veut accepter #ff0000 mais pas red, ou vice versa.

#[Assert\CssColor(formats: Assert\CssColor::HEX_LONG)]
private string $brandColor;

Cidr valide la notation CIDR pour IPv4 et IPv6, avec des options pour fixer la version et contraindre la plage de masque réseau. Les outils d’infrastructure et les formulaires de config réseau ont enfin une contrainte de première classe.

Le troisième ajout n’est pas une nouvelle contrainte. Ce sont les attributs imbriqués PHP 8.1 qui rendent les contraintes composées existantes utilisables sans XML. AtLeastOneOf, Collection, All, Sequentially : tout ça nécessitait auparavant des contournements d’annotation. Maintenant ça fonctionne juste comme attributs :

#[Assert\Collection(
    fields: [
        'email' => new Assert\Email(),
        'role'  => [new Assert\NotBlank(), new Assert\Choice(['admin', 'user'])],
    ]
)]
private array $payload;

Le sérialiseur, nettoyé

Deux choses. D’abord, le contexte de sérialisation est maintenant configurable globalement au lieu d’être répété à chaque appel serialize() :

# config/packages/serializer.yaml
serializer:
    default_context:
        enable_max_depth: true

Ensuite, l’option COLLECT_DENORMALIZATION_ERRORS change comment le sérialiseur gère les erreurs de type à la désérialisation. Au lieu de lever une exception au premier problème, il les collecte tous et les expose via PartialDenormalizationException. Si on écrit une API qui désérialise des corps de requête, c’est la différence entre retourner “le premier champ qui échoue” et “tous les champs qui échouent” dans une seule réponse.

Les utilitaires de string que personne ne savait vouloir

trimPrefix() et trimSuffix() sur les classes UnicodeString / ByteString. Pas glamour, mais supprimer un préfixe connu avec ltrim() est un piège subtil : ça supprime des caractères, pas des strings. Ceux-ci sont corrects :

use function Symfony\Component\String\u;

u('file-image-001.png')->trimPrefix('file-');   // 'image-001.png'
u('report.html.twig')->trimSuffix('.twig');     // 'report.html'

Aussi dans cette version : NilUlid pour les ULIDs à valeur zéro, perMonth() et perYear() sur RateLimiter pour quand les limites horaires n’ont pas de sens, et appendToFile() dans le composant Filesystem a reçu un paramètre LOCK_EX optionnel pour les écrivains concurrents.

Déboguer l’environnement

debug:dotenv est une nouvelle commande console qui montre quels fichiers .env ont été chargés et d’où vient chaque valeur. Quand on a .env, .env.local, .env.test, et .env.test.local qui se battent et que quelque chose ne va pas, cette commande dit exactement quel fichier a gagné. Elle n’apparaît que quand le composant Dotenv est utilisé, ce qui est le cas pour toute application Symfony standard.