Contact salesFree trial
Blog

Focus sur les fonctionnalités de PHP 8.0 : Attributs

PHPcaractéristiques
12 novembre 2020
Partager

Une histoire de changement

Certains changements enPHP sont proposés, discutés, implémentés et approuvés en peu de temps. Ils ne sont pas controversés, sont populaires, et ont un moyen naturel de les mettre en œuvre.

Et puis il y a ceux qui sont essayés, échouent, et reviennent plusieurs fois avant d'être finalement acceptés. Parfois, la mise en œuvre prend beaucoup de temps, parfois l'idée elle-même n'est qu'à moitié aboutie, et parfois la communauté ne s'est tout simplement pas encore montrée réceptive à une idée - "ce n'est pas encore le moment".

Les attributs font carrément partie de cette dernière catégorie. Ils ont été proposés pour lapremière fois en 2016, pour PHP 7.1, mais ont rencontré une résistance tenace et ont perdu le vote d'acceptation. Quatre ans plus tard, une proposition très similaire, bien que de portée légèrement réduite, a été adoptée avec un seul dissident. C'est apparemment une idée dont le temps est venu.

Le méta

Qu'est-ce qu'un attribut ? Les attributs sont des métadonnées déclaratives qui peuvent être attachées à d'autres parties du langage, puis analysées au moment de l'exécution pour contrôler le comportement.

Si vous avez déjà utilisé les Annotations Doctrine, les attributs sont essentiellement cela, mais en tant que citoyen de première classe dans la syntaxe du langage. Les termes "attributs" et "annotations" sont utilisés de manière interchangeable dans les différents langages qui les supportent. PHP a utilisé le terme "attribut" spécifiquement pour réduire la confusion avec les annotations Doctrine, puisqu'elles ont une syntaxe différente. Parce que les attributs sont intégrés dans le langage, plutôt que d'être simplement une convention de documentation dans l'espace utilisateur, ils peuvent être lus et vérifiés par les analyseurs statiques, les IDE, et les surligneurs de syntaxe.

La syntaxe spécifique a fait l'objet de bien plus de débats que la fonctionnalité elle-même, mais je vais passer sur tout cela et regarder le résultat final avec un exemple d'attributs PHP. Les attributs en PHP 8.0 empruntent leur syntaxe à Rust :

<?php #[GreedyLoad] class Product { #[Positive] protected int $id ; #[Admin] public function refillStock(int $quantity) : bool { // ... } }

Ces différents blocs #[...] sont des attributs. Au moment de l'exécution, ils ne font ... absolument rien. Ils n'ont aucun impact sur le code lui-même. Cependant, ils sont disponibles pour l'API de réflexion, qui permet à d'autres codes d'examiner la classeProduct, ou ses propriétés et méthodes, et de prendre des mesures supplémentaires. C'est à vous de décider quelle est cette action, car vous pouvez l'adapter à vos besoins.

Par ailleurs, les attributs sont interprétés comme des commentaires dans les anciennes versions de PHP et sont donc ignorés, s'ils sont sur une seule ligne. C'est plus un effet secondaire utile qu'une fonctionnalité délibérée, mais cela vaut la peine d'être noté. Il est probablement interprété comme un commentaire dans votre navigateur aussi. Il faudra un peu de temps pour que les surligneurs de syntaxe et les IDE rattrapent leur retard, mais cela ne saurait tarder.

Une remarque importante à propos des attributs est qu'ils ne sont pas limités aux chaînes de caractères. En fait, dans la grande majorité des cas, il s'agit d'objets, qui peuvent prendre des paramètres.

Les attributs personnalisés PHP peuvent être attachés à des classes, des propriétés, des méthodes, des fonctions, des constantes de classe, et même à des paramètres de fonction/méthode. C'est encore plus d'endroits que les annotations Doctrine.

Un exemple pratique

Tout ceci semble plutôt abstrait, alors prenons un exemple pratique des attributs PHP. La plupart des cas d'utilisation des attributs/annotations en PHP aujourd'hui tournent autour de l'enregistrement. C'est un moyen déclaratif d'indiquer à un système les détails d'un autre système, comme un plugin ou un système d'événements. A cette fin, j'ai mis à jour mon implémentation de PSR-14 Event Dispatcher, Tukio, pour supporter les attributs. Voici comment cela fonctionne (de manière quelque peu simplifiée).

Tukio fournit un "fournisseur d'auditeurs" qui regroupe et ordonne les "auditeurs", c'est-à-dire tout type d'objet appelable auquel un événement peut être transmis. Normalement, vous pouvez enregistrer un listener comme suit :

<?php function my_listener(MyEventType $event) : void { ... } $provider = new OrderedListenerProvider() ; $provider->addListener('my_listener', 5, 'listener_a', MyEventType::class) ;

Ces paramètres sont l'appelant à ajouter (dans ce cas, le nom de la fonction), puis éventuellement une priorité pour l'ordre, un identifiant personnalisé et le type d'événement à écouter. Ces deux derniers paramètres sont généralement dérivés automatiquement, mais je les inclus ici pour montrer que vous pouvez les spécifier à la volée. Il existe également des méthodes permettant d'ajouter des écouteurs avant ou après un autre écouteur, en fonction de son ID.

Spécifier toutes ces méthodes à la volée est un vrai casse-tête. Il serait particulièrement intéressant de spécifier l'ID en même temps que l'écouteur, par exemple, afin d'assurer la cohérence de l'ensemble. Les attributs nous permettent de le faire.

Tout d'abord, nous définissons notre nouvel attribut. Les attributs sont des classes PHP normales qui possèdent elles-mêmes un attribut spécial :

<?php namespace Crell\Tukio ; use \NAttribute ; #[Attribut] class Listener implements ListenerAttribute { public function __construct( public ?string $id = null, public ?string $type = null, ) {} }

L' attribut #[Attribut] indique à PHP "oui, cette classe peut être chargée en tant qu'attribut". Notez également que, comme l'attribut est lui-même une classe, il est soumis aux règles de l'espace de noms et doit être utilisé au début du fichier.

La classe définit également un constructeur ... en utilisant la nouvelle syntaxe de promotion des constructeurs. Il s'agit strictement d'une classe de données internes, donc nous lui donnerons juste deux propriétés publiques optionnelles qui seront remplies par le constructeur. (En PHP 7.4, le même code serait deux fois plus long).

Allons plus loin : il n'est pas logique de mettre un attributListener sur un paramètre ou une classe, mais seulement sur les fonctions et les méthodes. Nous pouvons donc indiquer à la classe qu'il s'agit d'un attribut valide uniquement sur les fonctions et les méthodes, à l'aide d'une série de drapeaux de bits :

<?php namespace Crell\Tukio ; use \NAttribute ; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] class Listener implémente ListenerAttribute { public function __construct( public ?string $id = null, public ?string $type = null, ) {} }

Maintenant, si nous essayons de mettre un attributListener sur une fonction ou une méthode qui n'en est pas une, nous obtiendrons une erreur lorsque nous essaierons de l'utiliser.

Cela suggère également comment nous utiliserons l'attributListener pour passer des paramètres.

<?php use Crell\Tukio\Listener ; #[Listener('listener_a')] function my_listener(MyEventType $event) : void { ... } $provider = new OrderedListenerProvider() ; $provider->addListener('my_listener', 5) ;

En soi, cela ne fait rien. Mais une fois qu'on nous a donné le nom de la fonction, nous pouvons utiliser l'API de réflexion pour extraire cette information. Une version (très) simplifiée de ce que Tukio fait ici est la suivante :

<?php Use Crell\Tukio\Listener ; /// Une chaîne signifie qu'il s'agit d'un nom de fonction, alors réfléchissez-y en tant que fonction.
if (is_string($listener)) { $ref = new \ReflectionFunction($listener) ; $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF) ; $attributes = array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs) ; }

Dans cet exemple d'attributs PHP, $attribs est un tableau d' objets\ReflectionAttribute. Il y a plusieurs choses que vous pouvez faire avec l'un d'entre eux. En particulier, l'attribut n'a pas besoin d 'être une classe ! Vous pouvez obtenir uniquement le nom et les arguments (s'il y en a) sous forme de chaîne de caractères et de tableau, respectivement. Cela peut être utile pour l'analyse statique ou pour de simples attributs de drapeau dont l'existence vous dit déjà tout ce que vous avez besoin de savoir.

La méthode getAttributes() peut également filtrer les attributs sur une valeur. Dans ce cas, nous la limitons pour qu'elle ne retourne que les attributs de Listener et pour permettre aux sous-classes de Listener d'être incluses également. Bien qu'optionnel, je recommande fortement de faire les deux car cela filtre naturellement les attributs d'autres bibliothèques que vous n'attendez pas, et autoriser les sous-classes ou les implémentations signifie que vous pouvez regrouper une série d'attributs à l'aide d'une interface.

La dernière ligne reprend ce tableau et appelle newInstance() sur chaque attribut. Cela appelle directement le constructeur de Listener avec les paramètres spécifiés, puis renvoie le résultat. L'objet qui revient est identique à celui que vous auriez obtenu si vous aviez écrit new Listener('listener_a'). Au-delà de cet appel au constructeur, la classe d'attributs peut faire ou avoir tout ce que n'importe quelle autre classe peut faire ou avoir. Vous pouvez lui donner une logique interne complexe ou non, gérer des valeurs par défaut, rendre certains paramètres obligatoires, etc.

Tukio peut maintenant combiner les données de l' appel à la méthode addListener() et de l' attributListener comme il le souhaite. Dans ce cas, l'effet est le même que si vous aviez spécifié l'ID dans l'appel de méthode plutôt que dans l'attribut.

Un seul élément de langage peut être marqué par plusieurs attributs, même par des attributs provenant de bibliothèques totalement différentes. Les attributs peuvent également être répétés sur un même élément linguistique ou non. Il est parfois judicieux d'autoriser deux fois le même attribut, parfois non. Ces deux possibilités peuvent être appliquées.

Il y a beaucoup d'autres choses que vous pouvez faire, et Tukio a en fait plusieurs attributs qu'il supporte pour différents cas d'utilisation et situations que je n'aborderai pas ici pour des raisons de brièveté, mais cela devrait vous donner un avant-goût de ce qui est possible.

Bientôt dans un framework près de chez vous

Le support des attributs apparaît déjà dans les frameworks. La prochaine version Symfony 5.2 va inclure des versions d'attributs pour la définition des routes, par exemple. Cela signifie que vous pourrez déclarer un contrôleur et le relier à une route comme suit :

<?php use Symfony\Component\Routing\Annotation\Route ; class SomeController { #[Route('/path', 'action_name')] public function someAction() { // ... } }

Pour l'essentiel, il s'agit simplement d'échanger la syntaxe des annotations Doctrine contre des attributs natifs pour atteindre le même objectif. Cependant, il s'agit également d'aller plus loin. Vous pouvez même contrôler quelles dépendances sont transmises aux paramètres devotre contrôleur via des attributs sur les paramètres, ce que Doctrine ne pouvait pas faire.

<?php namespace App\Controller ; use App\Entity\MyUser ; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController ; use Symfony\Component\Security\Http\Attribute\CurrentUser ; class SomeController extends AbstractController { public function index(#[CurrentUser] MyUser $user) { // ... } }

Cela indique aux résolveurs d'arguments de passer l'utilisateur actuel, en particulier, au paramètre$user.

Il ne fait aucun doute que les attributs seront de plus en plus utilisés dans d'autres frameworks et bibliothèques, maintenant qu'ils bénéficient d'un support natif de premier ordre dans le langage.

Arguments promus

Il est également important de noter comment les attributs fonctionnent sur les arguments promus des constructeurs. Rappelez-vous qu'en PHP 8.0, il est maintenant possible de spécifier une propriété d'objet et un argument de constructeur en même temps, quand l'un s'applique à l'autre, comme ceci :

<?php class Point { public function __construct(public int $x, public int $y) {} }

Cependant, les arguments et les propriétés peuvent avoir des attributs. Dans cet exemple, l'attribut fait-il référence à l'argument ou à la propriété ?

<?php class Point { public function __construct( #[Positive] public int $x, #[Positive] public int $y, ) {} }

Techniquement, cela peut être l'un ou l'autre, mais cela n'a pas toujours de sens pour l'un ou l'autre. Le moteur ne peut pas savoir ce qu'il est censé faire.

Pour la version 8.0, il a été décidé de faire les deux. L'attribut sera disponible à la fois sur la propriété et sur l'argument. Si cela n'a pas de sens pour votre application, filtrez-la dans votre propre logique d'application ou n'utilisez pas de promotion de constructeur pour ce seul paramètre, afin de pouvoir l'attribuer à un seul endroit et pas à l'autre.

Attributs natifs

En 8.0, il n'y a pas d'attributs qui signifient quoi que ce soit pour le moteur PHP, à part la classe\Attribute elle-même. Cependant, il est peu probable que cela reste ainsi. La RFC mentionne plusieurs attributs qui pourraient avoir une signification pour le moteur dans le futur.

Par exemple, un attribut #[Memoize] pourrait indiquer au moteur qu'une fonction ou une méthode est sûre pour la mise en cache, et le moteur pourrait alors automatiquement mettre en cache les résultats de cette fonction. Une autre option serait un attribut #[Jit] qui pourrait indiquer au moteur JIT qu'une fonction ou une classe donnée est un bon ou un mauvais candidat pour la compilation JIT ou modifier la façon dont elle est compilée. Peut-être même un attribut#[Inline] qui pourrait indiquer au compilateur qu'il devrait essayer d'intégrer un appel de fonction, ce qui permettrait de gagner du temps sur l'appel lui-même.

Aucun de ces attributs n'existe encore, mais ce sont des idées qui sont maintenant à la disposition des développeurs de moteurs pour améliorer le langage dans les versions futures.

Une longue liste de crédits

Les attributs ont été développés et modifiés au cours de plusieurs RFC.

Le RFC original a été rédigé par Benjamin Eberlei et Martin Schröder et utilisait une syntaxe différente empruntée à d'anciennes versions de Hack. Un RFC suivant, rédigé par les mêmes personnes, a légèrement modifié les caractéristiques.

La syntaxe a été modifiée deux fois par la suite, dans des RFCs de Theodore Brown et Martin Schröder, puis de Derick Rethans et Benjamin Eberlei, pour aboutir à la syntaxe finale.

Restez à l'écoute

La semaine prochaine, nous couvrirons la dernière des trois grandes fonctionnalités qui changeront le monde de PHP dans la version 8.0. En guise de teaser, les exemples Symfony ci-dessus ont été modifiés à partir de l'article du blog d'annonce. La syntaxe réelle qu'ils montrent ressemble à ceci :

<?php class SomeController { #[Route('/path', name : 'action')] public function someAction() { // ... } }

Que se passe-t-il dans cet attribut ? Restez à l'écoute...

Votre meilleur travail
est à l'horizon

Essai gratuit
Discord
© 2025 Platform.sh. All rights reserved.