PHP 8.0 : Attributs (3/4)

il y a 4 ans par David Grudl  

La version 8.0 de PHP est sortie. Elle est tellement pleine de nouveautés qu'aucune version précédente ne l'a été. Leur présentation a nécessité quatre articles distincts. Dans ce troisième, nous allons examiner les attributs.

Les attributs apportent une toute nouvelle façon d'écrire des métadonnées structurées pour les classes et tous leurs membres, ainsi que pour les fonctions, les closures et leurs paramètres. À cette fin, les commentaires phpDoc étaient utilisés jusqu'à présent, mais leur syntaxe a toujours été si libre et incohérente qu'il n'était pas possible de commencer à les traiter de manière automatisée. C'est pourquoi ils sont remplacés par des attributs avec une syntaxe fixe et une prise en charge dans les classes de réflexion.

Par conséquent, les bibliothèques qui obtenaient jusqu'à présent des métadonnées en analysant les commentaires phpDoc pourront les remplacer par des attributs. Un exemple est Nette, où dans les dernières versions d'Application et de DI, vous pouvez déjà utiliser les attributs Persistent, CrossOrigin et Inject au lieu des annotations @persistent, @crossOrigin et @inject.

Code utilisant des annotations :

/**
 * @persistent(comp1, comp2)
 */
class SomePresenter
{
	/** @persistent */
	public $page = 1;

	/** @inject */
	public Facade $facade;

	/**
	 * @crossOrigin
	 */
	public function handleSomething()
	{
	}
}

La même chose en utilisant des attributs :

use Nette\Application\Attributes\CrossOrigin;
use Nette\Application\Attributes\Persistent;
use Nette\DI\Attributes\Inject;

#[Persistent('comp1', 'comp2')]
class SomePresenter
{
	#[Persistent]
	public int $page = 1;

	#[Inject]
	public Facade $facade;

	#[CrossOrigin]
	public function handleSomething()
	{
	}
}

PHP évalue les noms des attributs de la même manière que s'il s'agissait de classes, c'est-à-dire dans le contexte de l'espace de noms et des clauses use. Ainsi, il serait possible de les écrire par exemple comme ceci :

use Nette\Application\Attributes;

class SomePresenter
{
	#[Attributes\Persistent]
	public int $page = 1;

	#[\Nette\DI\Attributes\Inject]
	public Facade $facade;

La classe représentant l'attribut peut exister ou non. Mais il est certainement préférable qu'elle existe, car l'éditeur peut alors la suggérer lors de l'écriture, l'analyseur statique peut détecter les fautes de frappe, etc.

Syntaxe

Ce qui est pratique, c'est que PHP avant la version 8 ne voit les attributs que comme des commentaires, ils peuvent donc être utilisés même dans du code qui doit fonctionner dans des versions plus anciennes.

L'écriture d'un attribut individuel ressemble à la création d'une instance d'objet, si nous omettions l'opérateur new. C'est-à-dire le nom de la classe suivi éventuellement d'arguments entre parenthèses :

#[Column('string', 32, true, false)]#
protected $username;

Et c'est ici que la nouvelle fonctionnalité de PHP 8.0 peut être mise à profit : les arguments nommés:

#[Column(
	type: 'string',
	length: 32,
	unique: true,
	nullable: false,
)]#
protected $username;

Chaque élément peut avoir plusieurs attributs, qui peuvent être écrits séparément ou séparés par une virgule :

#[Inject]
#[Lazy]
public Foo $foo;

#[Inject, Lazy]
public Bar $bar;

L'attribut suivant s'applique aux trois propriétés :

#[Common]
private $a, $b, $c;

Dans les valeurs par défaut des propriétés, on peut utiliser des expressions simples et des constantes qui peuvent être évaluées pendant la compilation, et il en va de même pour les arguments des attributs :

#[
	ScalarExpression(1 + 1),
	ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
	BitShift(4 >> 1, 4 << 1),
	BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]

Malheureusement, la valeur d'un argument ne peut pas être un autre attribut, c'est-à-dire qu'il n'est pas possible d'imbriquer les attributs. Par exemple, l'annotation suivante utilisée dans Doctrine ne peut pas être transformée de manière tout à fait directe en attributs :

/**
 * @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
 */

De même, il n'existe pas d'équivalent sous forme d'attribut pour le phpDoc de fichier, c'est-à-dire le commentaire situé au début du fichier, qui est utilisé par exemple par Nette Tester.

Récupération des attributs

Nous pouvons découvrir quels attributs possèdent les différents éléments à l'aide de la réflexion. Les classes de réflexion disposent d'une nouvelle méthode getAttributes(), qui renvoie un tableau d'objets ReflectionAttribute.

use MyAttributes\Example;

#[Example('Hello', 123)]
class Foo
{}

$reflection = new ReflectionClass(Foo::class);

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // nom complet de l'attribut, par ex. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // renvoie l'instance new MyAttributes\Example('Hello', 123)
}

Les attributs retournés peuvent être filtrés par paramètre, par exemple $reflection->getAttributes(Example::class) ne renverra que les attributs Example.

Classes d'attributs

La classe d'attribut MyAttributes\Example n'a pas besoin d'exister. L'existence n'est requise que par l'appel de la méthode newInstance() car elle crée son instance. Écrivons-la donc. Il s'agira d'une classe tout à fait ordinaire, il suffit d'y indiquer l'attribut Attribute (c'est-à-dire depuis l'espace de noms système global) :

namespace MyAttributes;

use Attribute;

#[Attribute]
class Example
{
	public function __construct(string $message, int $number)
	{
		...
	}
}

Il est possible de limiter les éléments de langage sur lesquels il sera légal d'utiliser l'attribut. Ainsi, par exemple, uniquement sur les classes et les propriétés :

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
	...
}

Les drapeaux disponibles sont TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER et la valeur par défaut TARGET_ALL.

Mais attention, la vérification de l'utilisation correcte ou incorrecte se produit étonnamment seulement lors de l'appel de la méthode newInstance(). Le compilateur lui-même n'effectue pas ce contrôle.

L'avenir avec les attributs

Grâce aux attributs et aux nouveaux types, les commentaires de documentation PHP deviendront pour la première fois de leur histoire de véritables commentaires de documentation. PhpStorm propose déjà ses propres attributs, qui peuvent remplacer par exemple l'annotation @deprecated. Et on peut supposer que cet attribut sera un jour standard en PHP. De même, d'autres annotations seront remplacées, comme @throws, etc.

Bien que Nette utilise des annotations depuis sa toute première version pour marquer les paramètres et composants persistants, leur utilisation plus massive n'a pas eu lieu car il ne s'agissait pas d'une construction native du langage, les éditeurs ne les suggéraient donc pas et il était facile de faire des erreurs. Bien que cela soit aujourd'hui résolu par des plugins d'éditeur, la voie vraiment native qu'apportent les attributs ouvre de toutes nouvelles possibilités.

D'ailleurs, les attributs ont obtenu une exception dans le Nette Coding Standard, qui exige que le nom de la classe, en plus de la spécificité (par exemple Product, InvalidValue), contienne également la généralité (donc ProductPresenter, InvalidValueException). Sinon, il ne serait pas clair lors de l'utilisation dans le code ce que la classe représente exactement. Pour les attributs, ce n'est au contraire pas souhaitable, la classe s'appelle donc Inject au lieu de InjectAttribute.

Dans la dernière partie, nous examinerons quelles nouvelles fonctions et classes sont apparues en PHP et nous présenterons le Just in Time Compiler.