PHP 8.0: Atributy (3/4)

před měsícem od David Grudl     edit

Vyšlo PHP verze 8.0. Je tak nadupané novinkami, jako nebyla žádná verze předtím. Jejich představení si vyžádalo rovnou čtyři samostatné články. V tomto třetím se podíváme na atributy.

Atributy přinášejí zcela nový způsob, jak zapisovat strukturovaná metadata ke třídám a všem jejím členům, taktéž funkcím, closurám a jejich parametrům. K tomuto účelu se dosud využívaly phpDoc komentáře, ale jejich syntax byla vždy natolik volná a nejednotná, že nebylo možné je začít strojově zpracovávat. Proto je nahrazují atributy s pevnou syntaxí a podporou v reflexních třídách.

Tudíž knihovny, které dosud získávaly metadata parsováním phpDoc komentářů, je budou moci nahradit za atributy. Příkladem je třeba Nette, kde v nejnovějších verzích Application a DI už můžete místo anotací @persistent, @crossOrigin a @inject používat atributy Persistent, CrossOrigin a Inject.

Kód používající anotace:

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

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

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

Totéž pomocí atributů:

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 vyhodnocuje názvy atributů stejně, jako by se jednalo o třídy, tedy v kontextu jmenného prostoru a klauzulí use. Tedy bylo by možné je zapsat kupříkladu i takto:

use Nette\Application\Attributes;

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

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

Třída představující atribut existovat může i nemusí. Je ale určitě lepší, pokud existuje, protože pak ji může editor napovídat při psaní, statický analyzátor rozpozná překlepy atd.

Syntax

Šikovné je, že PHP před verzí 8 vidí atributy jen jako komentáře, takže je lze používat i v kódu, který má fungovat ve starších verzích.

Zápis jednotlivého atributu vypadá jako vytváření instance objektu, pokud bychom vynechali operátor new. Tedy název třídy za kterým mohou následovat v závorkách argumenty:

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

A tady lze skvěle uplatnit další horkou novinku PHP 8.0 – pojmenované argumenty:

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

Každý element může mít více atributů, které lze zapsat samostatně nebo oddělené čárkou:

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

#[Inject, Lazy]
public Bar $bar;

Následující atribut platí pro všechny tři properties:

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

Ve výchozích hodnotách properties lze používat jednoduché výrazy a konstanty, které lze vyhodnotit během kompilace, a totéž platí i pro argumenty atributů:

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

Bohužel hodnotou argumentu nemůže být další atribut, tj. nelze atributy vnořovat. Například následující anotaci používanou v Doctrine nelze zcela přímočaře přetvořit do atributů:

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

Taktéž neexistuje atributová obdoba pro souborový phpDoc, tedy komentář nacházející se na začátku souboru, který využívá například Nette Tester.

Zjišťování atributů

Jaké atributy mají jednotlivé elementy zjistíme pomocí reflexe. Reflexní třídy disponují novou metodou getAttributes(), která vrátí pole objektů ReflectionAttribute.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // plný název atributu, např. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // vrací instanci new MyAttributes\Example('Hello', 123)
}

Vrácené atributy lze filtrovat parametrem, např. $reflection->getAttributes(Example::class) vrátí jen atributy Example.

Třídy atributů

Třída atributu MyAttributes\Example nemusí existovat. Existenci vyžaduje pouze volání metody newInstance() protože vytváří její instanci. Pojďme ji tedy napsat. Půjde o úplně běžnou třídu, jen u ní musíme uvést atribut Attribute (tj. z globálního systémového namespace):

namespace MyAttributes;

use Attribute;

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

Lze omezit, u jakých jazykových elementů bude legální atribut použít. Takto třeba jen u tříd a properties:

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

K dispozici jsou flagy TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER a výchozí TARGET_ALL.

Ale pozor, k ověření správného či nesprávného použití dochází překvapivě až při zavolání metody newInstance(). Samotný kompilátor tuto kontrolu neprovádí.

Budoucnost s atributy

Díky atributům a novým typům se z PHP dokumentačních komentářů poprvé v jejich historii stanou opravdu jen dokumentující komentáře. Už teď přichází PhpStorm s vlastními atributy, které mohou nahradit třeba anotaci @deprecated. A dá se předpokládat, že tento atribut bude jednou standardně v PHP. Podobně se nahradí i další anotace, jako @throws atd.

Ačkoliv Nette používá anotace od své úplně první verze pro označení persistentních parametrů a komponent, k jejich masivnějšímu využití nedošlo proto, že nešlo o nativní jazykový konstrukt, takže editory je nenapovídaly a bylo snadné v nich udělat chybu. Tohle sice dnes už řeší pluginy do editorů, ale opravdu nativní cesta, jakou přinášejí atributy, otevírá zcela nové možnosti.

Mimochodem, atributy získaly výjimku v Nette Coding Standard, který požaduje, aby název třídy kromě specifičnosti (např. Product, InvalidValue) obsahoval i obecnost (tedy ProductPresenter, InvalidValueException). Jinak by nebylo při užití v kódu zřejmé, co přesně třída představuje. U atributů tohle naopak není žádoucí, tedy třída se jmenuje Inject místo InjectAttribute.

V posledním díle se podíváme, jaké se v PHP objevily nové funkce a třídy a představíme si Just in Time Compiler.

Komentáře (RSS)

  1. Děkujeme. Je dobré, že to tak sepisuješ.

    před měsícem
  2. Fakt super práce!

    před měsícem
  3. Super, luxusní, díky!

    před měsícem

Chcete-li odeslat komentář, přihlaste se