PHP 8.0: Attribútumok (3/4)

4 éve írta David Grudl  

Megjelent a PHP 8.0-s verziója. Annyira tele van újdonságokkal, mint még egyetlen verzió sem korábban. Bemutatásukhoz egyenesen négy különálló cikkre volt szükség. Ebben a harmadikban az attribútumokat nézzük meg.

Az attribútumok teljesen új módot hoznak a strukturált metaadatok írására az osztályokhoz és azok minden tagjához, valamint a függvényekhez, closure-ökhöz és azok paramétereihez. Erre a célra eddig a phpDoc kommenteket használták, de azok szintaxisa mindig annyira laza és következetlen volt, hogy nem lehetett őket gépi feldolgozásra használni. Ezért helyettesítik őket az attribútumok, amelyek fix szintaxissal és a reflection osztályokban való támogatással rendelkeznek.

Ennek következtében azok a könyvtárak, amelyek eddig a phpDoc kommentek elemzésével nyertek metaadatokat, lecserélhetik őket attribútumokra. Példa erre a Nette, ahol a legújabb Application és DI verziókban már a @persistent, @crossOrigin és @inject annotációk helyett a Persistent, CrossOrigin és Inject attribútumokat használhatja.

Annotációkat használó kód:

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

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

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

Ugyanez attribútumokkal:

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()
	{
	}
}

A PHP az attribútumok neveit ugyanúgy értékeli, mintha osztályok lennének, tehát a névtér és a use klauzulák kontextusában. Tehát például így is le lehetne írni őket:

use Nette\Application\Attributes;

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

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

Az attribútumot reprezentáló osztály létezhet, de nem kötelező. De mindenképpen jobb, ha létezik, mert akkor a szerkesztő súgni tudja írás közben, a statikus elemző felismeri az elírásokat stb.

Szintaxis

Praktikus, hogy a PHP 8 előtti verziók az attribútumokat csak kommentként látják, így használhatók olyan kódban is, amelynek régebbi verziókban is működnie kell.

Egyetlen attribútum írása úgy néz ki, mint egy objektum példányosítása, ha kihagynánk a new operátort. Tehát az osztály neve, amelyet zárójelben argumentumok követhetnek:

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

És itt van az a hely, ahol a PHP 8.0 új, forró funkcióját, a named arguments használhatjuk:

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

Minden elemnek több attribútuma lehet, amelyeket külön-külön vagy vesszővel elválasztva lehet írni:

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

#[Inject, Lazy]
public Bar $bar;

A következő attribútum mindhárom property-re vonatkozik:

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

A property-k alapértelmezett értékeiben egyszerű kifejezéseket és konstansokat lehet használni, amelyeket fordítás közben lehet kiértékelni, és ugyanez vonatkozik az attribútumok argumentumaira is:

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

Sajnos az argumentum értéke nem lehet egy másik attribútum, azaz az attribútumokat nem lehet egymásba ágyazni. Például a Doctrine-ben használt következő annotációt nem lehet teljesen közvetlenül attribútumokká alakítani:

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

Szintén nincs attribútum megfelelője a phpDoc fájlnak, azaz a fájl elején található megjegyzésnek, amelyet például a Nette Tester használ.

Attribútumok lekérdezése

Hogy mely elemeknek milyen attribútumai vannak, azt a reflection segítségével tudjuk meg. A reflection osztályok új getAttributes() metódussal rendelkeznek, amely ReflectionAttribute objektumok tömbjét adja vissza.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // attribútum teljes neve, pl. MyAttributes\Example
	$attribute->getArguments(); // ['Szia', 123]
	$attribute->newInstance();  // visszaadja a new MyAttributes\Example('Szia', 123) példányt
}

A visszaadott attribútumokat paraméterrel lehet szűrni, pl. a $reflection->getAttributes(Example::class) csak az Example attribútumokat adja vissza.

Attribútum osztályok

Az MyAttributes\Example attribútum osztálynak nem kell léteznie. Létezését csak a newInstance() metódus hívása követeli meg, mert annak példányát hozza létre. Írjuk meg tehát. Egy teljesen átlagos osztály lesz, csak meg kell adnunk hozzá az Attribute attribútumot (azaz a globális rendszer névtérből):

namespace MyAttributes;

use Attribute;

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

Lehet korlátozni, hogy mely nyelvi elemeknél lesz legális az attribútum használata. Így például csak osztályoknál és property-knél:

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

Rendelkezésre állnak a TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER flag-ek és az alapértelmezett TARGET_ALL.

De vigyázat, a helyes vagy helytelen használat ellenőrzése meglepő módon csak a newInstance() metódus hívásakor történik meg. Maga a fordító ezt az ellenőrzést nem végzi el.

Jövő az attribútumokkal

Az attribútumoknak és az új típusoknak köszönhetően a PHP dokumentációs kommentek történetük során először válnak valóban csak dokumentáló kommentekké. Már most is a PhpStorm saját attribútumokkal áll elő, amelyek helyettesíthetik például a @deprecated annotációt. És feltételezhető, hogy ez az attribútum egyszer szabványosan a PHP-ban lesz. Hasonlóan helyettesítődnek majd más annotációk is, mint a @throws stb.

Bár a Nette a legelső verziójától kezdve használ annotációkat a perzisztens paraméterek és komponensek megjelölésére, masszívabb kihasználásukra nem került sor, mert nem natív nyelvi konstrukcióról volt szó, így a szerkesztők nem súgták őket, és könnyű volt bennük hibát ejteni. Ezt ma már ugyan megoldják a szerkesztő pluginok, de az igazán natív út, amelyet az attribútumok hoznak, teljesen új lehetőségeket nyit meg.

Mellesleg, az attribútumok kivételt kaptak a Nette Coding Standardban, amely megköveteli, hogy az osztály neve a specifikusságon (pl. Product, InvalidValue) kívül tartalmazza az általánosságot is (tehát ProductPresenter, InvalidValueException). Különben a kódban való használatkor nem lenne egyértelmű, mit is képvisel pontosan az osztály. Az attribútumoknál ez viszont nem kívánatos, tehát az osztály neve Inject InjectAttribute helyett.

Az utolsó részben megnézzük, milyen új függvények és osztályok jelentek meg a PHP-ban, és bemutatjuk a Just in Time Compilert.