PHP 8.0: Atribute (3/4)

acum 4 ani De la David Grudl  

Versiunea 8.0 a PHP este în curs de lansare chiar acum. Este plină de noutăți ca nicio altă versiune de până acum. Introducerea lor a meritat patru articole separate. În cea de-a treia parte vom arunca o privire asupra atributelor.

Atributele oferă o modalitate complet nouă de a scrie metadate structurate pentru clase și toți membrii acestora, precum și pentru funcții, închideri și parametrii acestora. Comentariile PhpDoc au fost folosite până acum în acest scop, dar sintaxa lor a fost întotdeauna atât de vagă și inconsecventă încât nu a fost posibil să se înceapă procesarea automată a acestora. Prin urmare, acestea sunt înlocuite de atribute cu o sintaxă determinată și cu suport în clasele de reflecție.

Din acest motiv, bibliotecile care au recuperat anterior metadatele prin analiza comentariilor phpDoc vor putea să le înlocuiască cu atribute. Un exemplu este Nette, unde în cele mai recente versiuni ale aplicației și DI se pot utiliza deja atributele Persistent, CrossOrigin și Inject în locul adnotărilor @persistent, @crossOrigin și @inject.

Codul care utilizează adnotări:

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

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

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

Același lucru cu atributele:

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, în contextul spațiilor de nume și al clauzelor use. Astfel, ar fi posibil chiar să le scriem, de exemplu, după cum urmează:

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 cu siguranță este mai bine dacă există, pentru că astfel editorul o poate sugera în timpul scrierii, analizorul static recunoaște greșelile de scriere etc.

Sintaxa

Este inteligent faptul că PHP înainte de versiunea 8 vede atributele doar ca fiind comentarii, astfel încât acestea pot fi utilizate și în codul care ar trebui să funcționeze în versiuni mai vechi.

Sintaxa unui atribut individual arată ca și cum am crea o instanță de obiect dacă omitem operatorul new. Așadar, numele clasei urmat de argumente opționale în 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 individual 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;

Expresiile și constantele simple, care pot fi evaluate în timpul compilării și care sunt utilizate ca valori implicite pentru proprietăți, pot fi utilizate ca argumente în atribute:

#[
	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, nu există o modalitate directă de a converti în atribute următoarea adnotare utilizată în Doctrine:

/**
 * @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.

Reflectarea atributelor

Atributele pe care le au elementele individuale pot fi determinate prin utilizarea reflecției. Clasele de reflecție au o nouă metodă getAttributes(), care returnează un tablou de obiecte 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)
}

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

Clase de atribute

Este posibil ca clasa de atribute MyAttributes\Example să nu existe. Doar un apel la o metodă newInstance() necesită existența acesteia, deoarece o instanțiază. Să o scriem deci. Va fi o clasă complet obișnuită, doar că trebuie să furnizăm atributul Attribute (adică din spațiul de nume al sistemului global):

namespace MyAttributes;

use Attribute;

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

Se poate restricționa pentru care elemente de limbă va fi permisă utilizarea atributului. De exemplu, doar pentru clase și proprietăți:

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

Sunt disponibile stegulețele TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER și implicit TARGET_ALL.

Atenție însă, verificarea utilizării corecte sau incorecte are loc în mod surprinzător numai atunci când este apelată metoda newInstance(). Compilatorul în sine nu efectuează această verificare.

Viitorul cu atribute

Datorită atributelor și noilor tipuri, comentariile din documentația PHP vor deveni, pentru prima dată în istoria lor, doar comentarii de documentare. PhpStorm vine deja cu atribute personalizate, care pot înlocui, de exemplu, adnotările @deprecated. Și se poate presupune că acest atribut va fi într-o zi în PHP în mod implicit. În mod similar, vor fi înlocuite și alte adnotări, cum ar fi @throws etc.

Deși Nette a folosit adnotări încă de la prima sa versiune pentru a indica parametrii și componentele persistente, acestea nu au fost folosite mai masiv deoarece nu erau o construcție nativă a limbajului, astfel încât editorii nu le sugerau și era ușor de făcut o greșeală. Chiar dacă acest aspect este deja abordat de plugin-urile editorilor, modul cu adevărat nativ, adus de atribute, deschide posibilități complet noi.

Apropo, atributele au obținut o excepție în standardul de codificare Nette, care cere ca numele clasei, pe lângă specificitate (de exemplu, Product, InvalidValue), să conțină și o generalitate (de exemplu, ProductPresenter, InvalidValueException). În caz contrar, atunci când este utilizat în cod, nu ar fi clar ce anume reprezintă clasa. Pentru atribute, acest lucru nu este de dorit, astfel încât 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 Compilatorul Just in Time.