PHP 8.0: Atribute (3/4)

acum 4 ani de David Grudl  

A fost lansată versiunea PHP 8.0. Este atât de plină de noutăți cum nu a mai fost nicio versiune înainte. Prezentarea lor a necesitat patru articole separate. În acest al treilea articol, vom analiza atributele.

Atributele aduc o modalitate complet nouă de a scrie metadate structurate pentru clase și toți membrii acestora, precum și pentru funcții, closure-uri și parametrii lor. În acest scop, s-au utilizat până acum comentariile phpDoc, dar sintaxa lor a fost întotdeauna atât de liberă și neunitară încât nu a fost posibil să se înceapă procesarea lor automată. De aceea, sunt înlocuite de atribute cu sintaxă fixă și suport în clasele de reflecție.

Prin urmare, bibliotecile care până acum obțineau metadate prin parsarea comentariilor phpDoc le vor putea înlocui cu atribute. Un exemplu este Nette, unde în cele mai recente versiuni ale Application și DI puteți utiliza deja atributele Persistent, CrossOrigin și Inject în loc de adnotările @persistent, @crossOrigin și @inject.

Cod care utilizează adnotări:

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

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

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

Același lucru folosind atribute:

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 evaluează numele atributelor la fel ca și cum ar fi clase, adică în contextul spațiului de nume și al clauzelor use. Deci, ar fi posibil să le scriem, de exemplu, și așa:

use Nette\Application\Attributes;

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

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

Clasa care reprezintă atributul poate exista sau nu. Dar este cu siguranță mai bine dacă există, deoarece atunci editorul o poate sugera la scriere, analizatorul static recunoaște greșelile de tipar etc.

Sintaxă

Este convenabil faptul că PHP înainte de versiunea 8 vede atributele doar ca pe niște comentarii, așa că pot fi utilizate și în codul care trebuie să funcționeze în versiuni mai vechi.

Scrierea unui atribut individual arată ca crearea unei instanțe de obiect, dacă am omite operatorul new. Adică numele clasei urmat eventual de argumente între paranteze:

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

Și aici este locul în care poate fi folosită noua caracteristică fierbinte a PHP 8.0 – argumente numite:

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

Fiecare element poate avea mai multe atribute, care pot fi scrise separat sau separate prin virgulă:

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

#[Inject, Lazy]
public Bar $bar;

Următorul atribut se aplică tuturor celor trei proprietăți:

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

În valorile implicite ale proprietăților se pot utiliza expresii simple și constante, care pot fi evaluate în timpul compilării, și același lucru este valabil și pentru argumentele atributelor:

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

Din păcate, valoarea unui argument nu poate fi un alt atribut, adică atributele nu pot fi imbricate. De exemplu, următoarea adnotare utilizată în Doctrine nu poate fi transformată direct în atribute:

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

De asemenea, nu există un atribut echivalent pentru fișierul phpDoc, adică un comentariu situat la începutul unui fișier, care este utilizat, de exemplu, de Nette Tester.

Detectarea atributelor

Ce atribute au elementele individuale aflăm folosind reflecția. Clasele de reflecție dispun de o nouă metodă getAttributes(), care returnează un array de obiecte ReflectionAttribute.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // numele complet al atributului, ex. MyAttributes\Example
	$attribute->getArguments(); // ['Salut', 123]
	$attribute->newInstance();  // returnează instanța new MyAttributes\Example('Salut', 123)
}

Atributele returnate pot fi filtrate după parametru, de exemplu, $reflection->getAttributes(Example::class) returnează doar atributele Example.

Clasele atributelor

Clasa atributului MyAttributes\Example nu trebuie să existe. Existența este necesară doar la apelarea metodei newInstance(), deoarece creează instanța sa. Haideți deci să o scriem. Va fi o clasă obișnuită, doar că trebuie să specificăm atributul Attribute (adică din spațiul de nume global al sistemului):

namespace MyAttributes;

use Attribute;

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

Se poate limita la ce elemente lingvistice va fi legal să se utilizeze atributul. Astfel, de exemplu, doar la clase și proprietăți:

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

Sunt disponibile flag-urile TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER și implicit TARGET_ALL.

Dar atenție, verificarea utilizării corecte sau incorecte are loc, surprinzător, abia la apelarea metodei newInstance(). Compilatorul însuși nu efectuează această verificare.

Viitorul cu atribute

Datorită atributelor și noilor tipuri, comentariile de documentare PHP devin, pentru prima dată în istoria lor, cu adevărat doar comentarii de documentare. Deja PhpStorm vine cu propriile atribute, care pot înlocui, de exemplu, adnotarea @deprecated. Și se poate presupune că acest atribut va fi odată standard în PHP. Similar se vor înlocui și alte adnotări, precum @throws etc.

Deși Nette utilizează adnotări de la prima sa versiune pentru a marca parametrii și componentele persistente, utilizarea lor masivă nu a avut loc deoarece nu era un construct lingvistic nativ, așa că editorii nu le sugerau și era ușor să se facă greșeli în ele. Acest lucru este rezolvat astăzi de pluginurile pentru editori, dar calea cu adevărat nativă, pe care o aduc atributele, deschide posibilități complet noi.

Apropo, atributele au obținut o excepție în Nette Coding Standard, care cere ca numele clasei, pe lângă specificitate (de ex. Product, InvalidValue), să conțină și generalitate (deci ProductPresenter, InvalidValueException). Altfel, la utilizarea în cod nu ar fi clar ce reprezintă exact clasa. La atribute, acest lucru nu este, în schimb, de dorit, deci clasa se numește Inject în loc de InjectAttribute.

În ultima parte vom analiza ce funcții și clase noi au apărut în PHP și vom prezenta Just in Time Compiler.