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

преди 3 години От David Grudl  

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

Атрибутите предоставят изцяло нов начин за записване на структурирани метаданни за класове и всички техни членове, както и за функции, затваряния и техните параметри. Досега за тази цел се използваха коментарите на 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/…at-s-new-1-4#…”:

#[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();      // full attribute name, e.g. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // returns an instance: 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, който изисква името на класа, освен специфичност (например Product, InvalidValue), да съдържа и обобщение (например ProductPresenter, InvalidValueException). В противен случай, когато се използва в кода, няма да е ясно какво точно представлява класът. За атрибутите това не е желателно, затова класът се нарича Inject вместо InjectAttribute.

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

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