PHP 8.0: Атрибути (3/4)

преди 4 години от David Grudl  

Излезе PHP версия 8.0. Тя е толкова натъпкана с новости, колкото никоя версия преди нея. Представянето им изисква цели четири отделни статии. В тази трета ще разгледаме атрибутите.

Атрибутите носят напълно нов начин за записване на структурирани метаданни към класове и всички техни членове, както и към функции, затваряния и техните параметри. За тази цел досега се използваха phpDoc коментари, но тяхната синтаксис винаги беше толкова свободна и нееднородна, че не беше възможно да започнат да се обработват машинно. Затова те се заменят с атрибути с фиксирана синтаксис и поддръжка в рефлексивните класове.

Следователно библиотеките, които досега получаваха метаданни чрез парсване на phpDoc коментари, ще могат да ги заменят с атрибути. Пример за това е Nette, където в най-новите версии на Application и DI вече можете вместо анотации @persistent, @crossOrigin и @inject да използвате атрибути Persistent, CrossOrigin и Inject.

Код, използващ анотации:

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

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

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

Същото с помощта на атрибути:

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 оценява имената на атрибутите по същия начин, както ако бяха класове, т.е. в контекста на пространството на имената и клаузите use. Тоест, би било възможно да ги запишем например и така:

use Nette\Application\Attributes;

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

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

Класът, представляващ атрибут, може да съществува или да не съществува. Но определено е по-добре, ако съществува, защото тогава редакторът може да го подсказва при писане, статичният анализатор ще разпознае печатни грешки и т.н.

Синтаксис

Удобно е, че PHP преди версия 8 вижда атрибутите само като коментари, така че те могат да се използват и в код, който трябва да работи в по-стари версии.

Записът на отделен атрибут изглежда като създаване на инстанция на обект, ако пропуснем оператора new. Тоест, името на класа, след което могат да последват аргументи в скоби:

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

И тук е мястото, където може да се използва новата гореща функция на PHP 8.0 – “именувани аргументи:https://blog.nette.org/…na-novostite#…”:

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

Всеки елемент може да има повече атрибути, които могат да бъдат записани поотделно или разделени със запетая:

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

#[Inject, Lazy]
public Bar $bar;

Следващият атрибут важи за всички три свойства:

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

В стойностите по подразбиране на свойствата могат да се използват прости изрази и константи, които могат да бъдат оценени по време на компилация, и същото важи и за аргументите на атрибутите:

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

За съжаление, стойността на аргумента не може да бъде друг атрибут, т.е. не могат да се влагат атрибути. Например, следната анотация, използвана в Doctrine, не може да бъде напълно директно преобразувана в атрибути:

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

Също така няма еквивалент на атрибут за файла phpDoc, т.е. коментар, разположен в началото на файла, който се използва например от Nette Tester.

Извличане на атрибути

Какви атрибути имат отделните елементи, установяваме с помощта на рефлексия. Рефлексивните класове разполагат с нов метод getAttributes(), който връща масив от обекти ReflectionAttribute.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // пълно име на атрибута, напр. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // връща инстанция new MyAttributes\Example('Hello', 123)
}

Върнатите атрибути могат да се филтрират по параметър, напр. $reflection->getAttributes(Example::class) ще върне само атрибути Example.

Класове на атрибути

Класът на атрибута MyAttributes\Example не е задължително да съществува. Съществуването се изисква само при извикване на метода newInstance(), защото той създава негова инстанция. Нека тогава да го напишем. Това ще бъде напълно обикновен клас, само при него трябва да посочим атрибута Attribute (т.е. от глобалното системно пространство на имената):

namespace MyAttributes;

use Attribute;

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

Може да се ограничи за кои езикови елементи ще бъде легално да се използва атрибутът. Така например само за класове и свойства:

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

Налични са флаговете TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER и по подразбиране TARGET_ALL.

Но внимавайте, проверката за правилна или неправилна употреба се извършва изненадващо чак при извикване на метода newInstance(). Самият компилатор не извършва тази проверка.

Бъдеще с атрибути

Благодарение на атрибутите и новите типове коментарите към документацията на PHP за първи път в своята история ще станат наистина само документални коментари. PhpStorm вече разполага с потребителски атрибути, които могат да заменят например анотацията @deprecated. И може да се предположи, че този атрибут един ден ще бъде в PHP по подразбиране. По същия начин ще бъдат заменени и други анотации, като например @throws и т.н.

Въпреки че Nette използва анотации от своята първа версия за обозначаване на персистентни параметри и компоненти, до тяхното по-масивно използване не се стигна, защото не бяха нативен езиков конструкт, така че редакторите не ги подсказваха и беше лесно да се направи грешка в тях. Това днес вече се решава от плъгини за редактори, но наистина нативният път, който носят атрибутите, отваря напълно нови възможности.

Между другото, атрибутите получиха изключение в Nette Coding Standard, който изисква името на класа освен специфичност (напр. Product, InvalidValue) да съдържа и общност (т.е. ProductPresenter, InvalidValueException). В противен случай при употреба в кода нямаше да е ясно какво точно представлява класът. При атрибутите това обратно не е желателно, т.е. класът се нарича Inject вместо InjectAttribute.

В последния дял ще разгледаме какви нови функции и класове се появиха в PHP и ще представим Just in Time Compiler.

Последни публикации