PHP 8.0: Attribútumok (3/4)

3 éve A címről David Grudl  

A PHP 8.0-ás verziója most jelenik meg. Tele van olyan újdonságokkal, mint egyetlen korábbi verzió sem. Bevezetésük négy külön cikket érdemelt volna. A harmadik részben az attribútumokat vesszük szemügyre.

Az attribútumok egy teljesen új módot biztosítanak az osztályok és minden tagjuk, valamint a függvények, zárlatok és paramétereik strukturált metaadatainak megírására. Erre a célra eddig is használták a PhpDoc kommenteket, de ezek szintaxisa mindig is olyan laza és következetlen volt, hogy nem lehetett gépi feldolgozásukba kezdeni. Ezért ezeket a reflection osztályokban meghatározott szintaxissal és támogatással rendelkező attribútumokkal váltják fel.

Emiatt azok a könyvtárak, amelyek korábban a phpDoc kommentek elemzése révén szereztek metaadatokat, képesek lesznek ezeket attribútumokkal helyettesíteni. Erre példa a Nette, ahol az Application és a DI legújabb verzióiban már a Persistent, CrossOrigin és Inject attribútumokat használhatjuk a @persistent, @crossOrigin és @inject megjegyzések helyett.

Az 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 vonatkozik az attribútumokra is:

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 ugyanúgy értékeli az attribútumneveket, mintha azok osztályok lennének, a névterek és a use záradékok kontextusában. Így akár az is lehetséges lenne, hogy például a következőképpen írjuk le ő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 vagy nem létezhet. De mindenképpen jobb, ha létezik, mert akkor a szerkesztő az írás során javasolhatja, a statikus elemző felismeri a gépelési hibákat stb.

Szintaxis

Okos dolog, hogy a 8-as verzió előtti PHP az attribútumokat csak megjegyzésként látja, így olyan kódban is használhatók, amelynek a régebbi verziókban is működnie kell.

Az egyedi attribútum szintaxisa úgy néz ki, mintha egy objektumpéldányt hoznánk létre, ha kihagyjuk a new operátort. Tehát az osztály neve, amelyet zárójelben opcionális argumentumok követnek:

#[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 is lehet, amelyeket külön-külön vagy vesszővel elválasztva lehet leírni:

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

#[Inject, Lazy]
public Bar $bar;

A következő attribútum mindhárom tulajdonságra vonatkozik:

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

A fordítás során kiértékelhető és a tulajdonságok alapértelmezett értékeiként használt egyszerű kifejezések és konstansok használhatók az attribútumok argumentumaként:

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

Sajnos egy argumentum értéke nem lehet egy másik attribútum, azaz az attribútumok nem lehetnek egymásba ágyazva. Például a Doctrine-ban használt következő annotáció nem alakítható át egyszerűen attribútumokká:

/**
 * @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útum tükrözés

Az, hogy az egyes elemek milyen attribútumokkal rendelkeznek, a tükrözés segítségével határozható meg. A reflexiós osztályok rendelkeznek egy új getAttributes() metódussal, amely egy objektumtömböt ad vissza 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)
}

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

Attribútum osztályok

A MyAttributes\Example attribútumosztály nem létezhet. Csak egy metódushívás newInstance() megköveteli a létezését, mert instanciálja. Írjuk tehát meg. Ez egy teljesen hétköznapi osztály lesz, csak az attribútumot Attribute (azaz a globális rendszer névteréből) kell megadnunk:

namespace MyAttributes;

use Attribute;

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

Korlátozható, hogy mely nyelvi elemeknél lesz engedélyezett az attribútum használata. Például csak az osztályok és tulajdonságok esetében:

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

A TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER és az alapértelmezett TARGET_ALL zászlók állnak rendelkezésre.

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

A jövő az attribútumokkal

Az attribútumoknak és az új típusoknak köszönhetően a PHP dokumentációs megjegyzések történelmük során először valóban csak dokumentációs megjegyzésekké válnak. A PhpStorm már most is rendelkezik egyéni attribútumokkal, amelyek helyettesíthetik például az annotációkat @deprecated. És feltételezhető, hogy ez az attribútum egy nap alapértelmezetten is benne lesz a PHP-ban. Hasonlóképpen más annotációk is helyettesíthetők lesznek, mint például a @throws stb.

Bár a Nette már az első verziója óta használ annotációkat a perzisztens paraméterek és komponensek jelölésére, nem használták tömegesebben, mert nem volt anyanyelvi konstrukció, így a szerkesztők nem javasolták őket, és könnyű volt hibázni. Bár ezt már kezelik a szerkesztő pluginok, a valóban natív mód, amit az attribútumok hoznak, teljesen új lehetőségeket nyit meg.

Az attribútumok egyébként kivételt nyertek a Nette kódolási szabványban, amely megköveteli, hogy az osztálynév a specifikusság mellett (pl. Product, InvalidValue) tartalmazzon egy általánosságot is (pl. ProductPresenter, InvalidValueException). Ellenkező esetben a kódban való használat során nem lenne egyértelmű, hogy az osztály pontosan mit képvisel. Az attribútumok esetében ez nem kívánatos, ezért az osztály neve InjectAttribute helyett Inject.

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