PHP 8.0: Atributi (3/4)

pred 3 leti od David Grudl  

Različica PHP 8.0 je pravkar izšla. Polna je novosti kot še nobena druga različica. Njihova predstavitev si zasluži štiri ločene članke. V tretjem delu si bomo ogledali atribute.

Atributi omogočajo povsem nov način zapisovanja strukturiranih metapodatkov za razrede in vse njihove člane ter funkcije, zapore in njihove parametre. Doslej smo v ta namen uporabljali komentarje PhpDoc, vendar je bila njihova sintaksa vedno tako ohlapna in nedosledna, da jih ni bilo mogoče začeti strojno obdelovati. Zato jih nadomeščajo atributi z določeno sintakso in podporo v razredih za razmislek.

Zaradi tega bodo knjižnice, ki so doslej metapodatke pridobivale z razčlenjevanjem komentarjev phpDoc, te lahko nadomestile z atributi. Primer je Nette, kjer lahko v najnovejših različicah aplikacij in DI namesto opomb @persistent, @crossOrigin in @inject že uporabljate atribute Persistent, CrossOrigin in Inject.

Koda z uporabo opomb:

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

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

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

Enako velja za 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 vrednoti imena atributov enako, kot če bi šlo za razrede, v okviru imenskih prostorov in stavkov use. Tako bi jih bilo mogoče zapisati na primer takole:

use Nette\Application\Attributes;

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

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

Razred, ki predstavlja atribut, lahko obstaja ali pa tudi ne. Vsekakor pa je bolje, če obstaja, saj ga lahko urednik predlaga med pisanjem, statični analizator prepozna tipkarske napake itd.

Sintaksa

Pametno je, da PHP pred različico 8 atribute vidi le kot komentarje, zato jih je mogoče uporabiti tudi v kodi, ki bi morala delovati v starejših različicah.

Sintaksa posameznega atributa je videti kot ustvarjanje instance predmeta, če izpustimo operator new. Torej ime razreda, ki mu sledijo neobvezni argumenti v oklepajih:

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

In tu je mesto, kjer lahko uporabimo novo vročo funkcijo PHP 8.0 – poimenovane argumente:

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

Vsak element ima lahko več atributov, ki jih lahko zapišemo posamezno ali pa jih ločimo z vejico:

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

#[Inject, Lazy]
public Bar $bar;

Naslednji atribut velja za vse tri lastnosti:

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

Enostavni izrazi in konstante, ki se lahko ovrednotijo med sestavljanjem in se uporabljajo kot privzete vrednosti lastnosti, se lahko uporabijo kot argumenti v atributih:

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

Vrednost argumenta žal ne more biti drug atribut, tj. atributov ni mogoče gnezditi. Na primer, ni preprostega načina, da bi naslednjo opombo, ki se uporablja v programu Doctrine, pretvorili v atribute:

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

Prav tako ni ekvivalenta atributa za datoteko phpDoc, tj. komentar, ki se nahaja na začetku datoteke in ga uporablja na primer Nette Tester.

Odraz atributa

Katere atribute imajo posamezni elementi, lahko določite z uporabo refleksije. Razredi za odboj imajo novo metodo getAttributes(), ki vrne polje predmetov 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)
}

Vrnjene atribute lahko filtriramo s parametrom, npr. $reflection->getAttributes(Example::class) vrne samo atribute Example.

Razredi atributov

Razred atributov MyAttributes\Example morda ne obstaja. Le klic metode newInstance() zahteva njegov obstoj, saj ga instancira. Zato ga zapišimo. To bo povsem običajen razred, le da moramo zagotoviti atribut Attribute (tj. iz globalnega sistemskega imenskega prostora):

namespace MyAttributes;

use Attribute;

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

Lahko omejimo, za katere jezikovne elemente bo uporaba atributa dovoljena. Na primer samo za razrede in lastnosti:

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

TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER in privzeto TARGET_ALL.

Toda pozor, preverjanje pravilne ali nepravilne uporabe se presenetljivo zgodi le ob klicu metode newInstance(). Sam prevajalnik tega preverjanja ne opravi.

Prihodnost z atributi

Zaradi atributov in novih tipov bodo komentarji dokumentacije PHP prvič v svoji zgodovini resnično postali le dokumentarni komentarji. PhpStorm že ima na voljo atribute po meri, ki lahko nadomestijo na primer anotacijo @deprecated. In domnevamo lahko, da bo ta atribut nekega dne v PHP privzeto uporabljen. Podobno bodo nadomeščene tudi druge opombe, kot je @throws itd.

Čeprav Nette že od svoje prve različice uporablja anotacije za označevanje trajnih parametrov in komponent, se te niso uporabljale množičneje, ker niso bile izvorni konstrukt jezika, zato jih uredniki niso predlagali in se je bilo enostavno zmotiti. Čeprav se tega že lotevajo vtičniki za urejevalnike, pa resnično domač način, ki ga prinašajo atributi, odpira povsem nove možnosti.

Mimogrede, atributi so dobili izjemo v kodirnem standardu Nette, ki zahteva, da ime razreda poleg specifičnosti (npr. Product, InvalidValue) vsebuje tudi splošnost (npr. ProductPresenter, InvalidValueException). V nasprotnem primeru pri uporabi v kodi ne bi bilo jasno, kaj točno razred predstavlja. Pri atributih to ni zaželeno, zato se razred imenuje Inject namesto InjectAttribute.

V zadnjem delu si bomo ogledali, katere nove funkcije in razredi so se pojavili v jeziku PHP, in predstavili prevajalnik Just in Time Compiler.