Symfony 7.0 est sorti le 29 novembre 2023, le même jour que 6.4. Le pattern tient : la version X.0 coupe le code déprécié et élève le plancher PHP. 7.0 exige PHP 8.2 et supprime tout ce que 6.4 avait marqué comme déprécié.

La suppression la plus visible : les annotations Doctrine. @Route, @ORM\Column, @Assert — disparus. Les attributs PHP natifs sont l’approche recommandée depuis Symfony 5.2. 7.0 rend juste ça officiel.

Les attributs partout

La migration des annotations vers les attributs est principalement mécanique : la syntaxe passe de @ à #[], et les références de classes passent des classes d’annotation Doctrine aux classes d’attribut PHP :

// avant
/** @Route('/users', methods={"GET"}) */

// après
#[Route('/users', methods: ['GET'])]

Le vrai gain n’est pas juste la syntaxe : les attributs sont validés par le moteur PHP, pas un parseur de docblock. Les IDEs peuvent les résoudre sans plugins personnalisés. Les outils d’analyse statique les comprennent nativement. Fini les “ça échoue silencieusement à l’exécution à cause d’une faute de frappe dans un commentaire.”

Workflow avec attributs PHP

Les listeners et guards d’événements Workflow peuvent maintenant être enregistrés via des attributs :

#[AsGuard(workflow: 'order', transition: 'ship')]
public function canShip(Event $event): void
{
    if (!$event->getSubject()->isPaymentConfirmed()) {
        $event->setBlocked(true);
    }
}

Le profiler de workflow, un panneau dédié montrant le marquage courant et les transitions disponibles, est un outil de debug vraiment utile quand on travaille avec des machines à états complexes.

DatePoint dans le composant Clock

DatePoint, le DateTime immutable avec gestion stricte des erreurs introduit dans 6.4, est maintenant la façon recommandée de travailler avec les dates. Combiné avec les propriétés readonly de PHP 8.2, les objets-valeur de dates dans le code de domaine deviennent presque trivialement propres :

readonly class Order {
    public function __construct(
        public DatePoint $createdAt,
        public ?DatePoint $shippedAt = null,
    ) {}
}

Ce que 7.0 supprime

La liste complète des suppressions : le support des annotations Doctrine, le bridge du composant Templating, le bridge ProxyManager, le bridge Monolog pour les versions inférieures à 3.0, et le transport Sendinblue (remplacé par Brevo). Le support PHP 8.0 et 8.1 se termine aussi. 8.2 est le plancher maintenant.

Monter de 6.4 avec toutes les notices de dépréciation corrigées, et 7.0 est fluide. Sauter cette étape et on s’expose à une mauvaise surprise.

Scheduler et AssetMapper diplômés

Deux composants qui sont sortis en expérimental dans 6.3 sont maintenant stables : Scheduler et AssetMapper. Stable signifie des APIs verrouillées, plus de mises en garde @experimental, et ils apparaissent correctement dans le guide de mise à jour. On peut vraiment compter sur eux maintenant.

Scheduler reçoit #[AsCronTask] et #[AsPeriodicTask] pour l’enregistrement de tâches par attribut, la modification de planning à l’exécution avec recalcul du heap, FailureEvent, et une option --date sur schedule:debug. AssetMapper ajoute le support des fichiers CSS dans l’importmap, une commande outdated, une commande audit, et le préchargement automatique via WebLink.

#[AsCronTask('0 2 * * *')]
class NightlyReportMessage {}

#[AsPeriodicTask(frequency: '1 hour')]
class HourlyCleanupMessage {}

Le câblage de services reçoit deux nouveaux attributs

#[AutowireLocator] et #[AutowireIterator] ont atterri dans 6.4 et sont stables dans 7.0. Ils remplacent la configuration verbose XML/YAML des service locators taggués par quelque chose qu’on peut juste mettre directement en PHP :

class HandlerRegistry
{
    public function __construct(
        #[AutowireLocator('app.handler', indexAttribute: 'key')]
        private ContainerInterface $handlers,
    ) {}
}

#[Target] est aussi plus intelligent : quand un service a un alias d’autowiring nommé comme invoice.lock.factory, on peut maintenant écrire #[Target('invoice')] au lieu du nom complet de l’alias. Moins de bruit quand le type dit déjà ce qu’on veut.

Messenger reçoit une gestion plus précise des échecs

RejectRedeliveredMessageException dit au worker de ne pas retenter un message, ce qui est pratique quand un message arrive deux fois à cause d’un timeout d’ack du transport et qu’on a besoin d’une sémantique exactly-once. messenger:failed:remove --all vide tout le transport d’échec en un coup, pas de boucle nécessaire. Les retries échoués peuvent aussi aller directement au transport d’échec, en contournant entièrement la queue de retry.

Plusieurs hôtes Redis Sentinel sont maintenant supportés dans le DSN :

redis-sentinel://host1:26379,host2:26379,host3:26379/mymaster

Console reçoit les noms de signaux et le profilage de commandes

SignalMap mappe les entiers de signaux à leurs noms POSIX. Quand un worker attrape SIGTERM, le log dit maintenant SIGTERM au lieu de 15. Petite chose, vraie amélioration. ConsoleTerminateEvent est dispatché même quand le processus se termine via signal, ce qui n’était pas le cas avant 7.0.

Le profilage de commandes arrive aussi : passer --profile à bin/console et les données collectées vont directement dans le profiler Symfony, navigable depuis l’UI web.

Form : des petites choses qui s’accumulent

ChoiceType reçoit une option duplicate_preferred_choices. La définir à false et on arrête de montrer la même option deux fois quand les choix préférés chevauchent la liste complète. FormEvent::setData() est déprécié pour les événements où les données sont déjà verrouillées à ce point du cycle de vie. La barre oblique auto-fermante sur les éléments <input> est aussi supprimée : <input> est un élément void en HTML5 et la barre oblique était techniquement invalide.

Le support des enums dans les formulaires est bien fait : ChoiceType rend les backed enums directement, et les enums translatable reçoivent leurs labels via le translator sans câblage personnalisé.

HttpFoundation : small but useful

Response::send() reçoit un paramètre $flush. Passer false pour bufferiser la sortie sans la flusher au client, utile quand on enchaîne des middlewares qui doivent inspecter la réponse avant qu’elle quitte le processus.

UriSigner passe de HttpKernel à HttpFoundation, où il appartient sémantiquement. Même nom de classe, namespace différent.

Les cookies reçoivent le support CHIPS (Cookies Having Independent Partitioned State), le mécanisme navigateur pour les cookies cross-site dans une partition first-party. Ça ne compte que si on construit des widgets embarquables, mais bon à savoir que c’est là.

Translation : provider Phrase et sortie arborescente

Phrase rejoint Crowdin et Lokalise comme provider de traduction supporté. Le configurer dans config/packages/translation.yaml et les commandes translation:push / translation:pull gèrent la synchronisation.

translation:pull reçoit une option --as-tree qui écrit les fichiers de traduction en YAML imbriqué plutôt qu’en clés à notation pointée plate. Si c’est vraiment mieux dépend entièrement de l’équipe.

LocaleSwitcher::runWithLocale() passe maintenant la locale courante comme argument au callback, évitant un appel getLocale() à l’intérieur :

$switcher->runWithLocale('fr', function (string $locale) use ($mailer) {
    $mailer->send($this->buildEmail($locale));
});

Quelques choses dans Serializer et DomCrawler

L’attribut Context du Serializer peut maintenant cibler des classes spécifiques, donc un seul DTO peut se comporter différemment pendant la (dé)sérialisation selon quelle classe détient le contexte. TranslatableNormalizer arrive pour normaliser les objets qui implémentent TranslatableInterface : le translator est appelé pendant la normalisation, pas avant.

Crawler::attr() reçoit un paramètre $default. Au lieu de null-checker la valeur de retour, on passe une valeur de repli :

$src = $crawler->attr('src', '/placeholder.png');

assertAnySelectorText() et assertAnySelectorTextContains() rejoignent l’ensemble d’assertions DomCrawler. Ils passent si au moins un élément correspondant satisfait la condition, plutôt que d’en exiger que tous correspondent.

HttpClient : réponses HAR pour les tests

MockResponse accepte maintenant les fichiers HAR (HTTP Archive). Enregistrer de vraies interactions HTTP dans le navigateur ou avec un proxy, déposer le fichier .har dans les fixtures de tests, et les rejouer :

$client = new MockHttpClient(HarFileResponseFactory::createFromFile(__DIR__.'/fixtures/api.har'));

Bien mieux qu’écrire des stubs de réponse à la main quand on traite avec une API complexe.