PHP 8.0 : Attributs (3/4)

il y a 4 ans de David Grudl  

La version 8.0 de PHP sort en ce moment même. Elle est pleine de nouveautés comme aucune autre version auparavant. Leur introduction méritait quatre articles distincts. Dans la troisième partie, nous allons jeter un coup d'oeil aux attributs.

Les attributs offrent 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 fermetures et leurs paramètres. Les commentaires PhpDoc ont été utilisés à cette fin jusqu'à présent, mais leur syntaxe a toujours été si lâche et incohérente qu'il n'était pas possible de commencer à les traiter par machine. Ils sont donc remplacés par des attributs dont la syntaxe est déterminée et qui sont pris en charge par les classes de réflexion.

Pour cette raison, les bibliothèques qui récupéraient auparavant les 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 DI vous pouvez déjà utiliser les attributs Persistent, CrossOrigin et Inject au lieu des annotations @persistent, @crossOrigin et @inject.

Code utilisant les annotations :

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

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

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

La même chose avec les 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 d'attributs de la même manière que s'il s'agissait de classes, dans le contexte des espaces de noms et des clauses use. Il serait donc même possible de les écrire, par exemple, comme suit :

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 vaut mieux qu'elle existe, car l'éditeur peut alors la suggérer pendant l'écriture, l'analyseur statique reconnaît les fautes de frappe, etc.

Syntaxe

Il est astucieux que PHP avant la version 8 ne voit les attributs que comme des commentaires, donc ils peuvent aussi être utilisés dans du code qui devrait fonctionner dans des versions plus anciennes.

La syntaxe d'un attribut individuel ressemble à la création d'une instance d'objet si l'on omet l'opérateur new. Donc, le nom de la classe suivi d'arguments optionnels 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 individuellement 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;

Les expressions simples et les constantes, qui peuvent être évaluées lors de la compilation et sont utilisées comme valeurs par défaut pour les propriétés, peuvent être utilisées comme arguments dans les 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 que les attributs ne peuvent pas être imbriqués. Par exemple, il n'y a pas de moyen direct de convertir l'annotation suivante utilisée dans Doctrine en attributs :

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

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

Réflexion sur l'attribut

Les attributs que possèdent les éléments individuels peuvent être déterminés en utilisant la réflexion. Les classes Reflection 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();      // full attribute name, e.g. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // returns an instance: new MyAttributes\Example('Hello', 123)
}

Les attributs renvoyés peuvent être filtrés par un paramètre, par exemple, $reflection->getAttributes(Example::class) renvoie uniquement les attributs Example.

Classes d'attributs

La classe d'attributs MyAttributes\Example peut ne pas exister. Seul un appel de méthode newInstance() nécessite son existence car il l'instancie. Alors, écrivons-la. Ce sera une classe tout à fait ordinaire, seulement nous devons fournir l'attribut Attribute (c'est-à-dire de l'espace de noms du système global) :

namespace MyAttributes;

use Attribute;

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

Il est possible de restreindre les éléments de langage pour lesquels l'utilisation de l'attribut sera autorisée. Par exemple, uniquement pour les classes et les propriétés :

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

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

Mais attention, la vérification de l'utilisation correcte ou incorrecte ne se produit étonnamment que lorsque la méthode newInstance() est appelée. Le compilateur lui-même n'effectue pas cette vérification.

Le futur avec les attributs

Grâce aux attributs et aux nouveaux types, les commentaires de la documentation PHP, pour la première fois de leur histoire, deviendront réellement des commentaires documentaires. PhpStorm est déjà livré avec des attributs personnalisés, qui peuvent remplacer, par exemple, l'annotation @deprecated. Et l'on peut supposer que cet attribut sera un jour en PHP par défaut. De même, d'autres annotations seront remplacées, comme @throws etc.

Bien que Nette utilise les annotations depuis sa toute première version pour indiquer les paramètres et les composants persistants, elles n'ont pas été utilisées plus massivement car elles n'étaient pas une construction du langage natif, les éditeurs ne les suggéraient donc pas et il était facile de faire une erreur. Bien que ce problème ait déjà été résolu par des plugins d'éditeur, la méthode réellement native, qui est apportée par les attributs, ouvre de toutes nouvelles possibilités.

À propos, les attributs ont obtenu une exception dans la norme de codage Nette, qui exige que le nom de la classe, en plus de la spécificité (par exemple Product, InvalidValue), contienne également une généralité (par exemple ProductPresenter, InvalidValueException). Sinon, lorsqu'il est utilisé dans le code, il ne serait pas clair ce que la classe représente exactement. Pour les attributs, ce n'est pas souhaitable, c'est pourquoi la classe est appelée Inject au lieu de InjectAttribute.

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