PHP 8.0: Atrybuty (3/4)
Ukazała się wersja PHP 8.0. Jest tak naładowana nowościami, jak żadna wersja wcześniej. Ich przedstawienie wymagało aż czterech oddzielnych artykułów. W tym trzecim przyjrzymy się atrybutom.

Atrybuty wprowadzają zupełnie nowy sposób zapisywania strukturalnych metadanych do klas i wszystkich ich członków, a także funkcji, domknięć (closures) i ich parametrów. Do tego celu dotychczas wykorzystywano komentarze phpDoc, ale ich składnia była zawsze na tyle luźna i niejednolita, że nie było możliwe ich maszynowe przetwarzanie. Dlatego zastępują je atrybuty ze stałą składnią i wsparciem w klasach refleksyjnych.
Dzięki temu biblioteki, które dotychczas pozyskiwały metadane poprzez
parsowanie komentarzy phpDoc, będą mogły zastąpić je atrybutami.
Przykładem jest na przykład Nette, gdzie w najnowszych wersjach Application
i DI można już zamiast adnotacji @persistent
,
@crossOrigin
i @inject
używać atrybutów
Persistent
, CrossOrigin
i Inject
.
Kod używający adnotacji:
/**
* @persistent(comp1, comp2)
*/
class SomePresenter
{
/** @persistent */
public $page = 1;
/** @inject */
public Facade $facade;
/**
* @crossOrigin
*/
public function handleSomething()
{
}
}
To samo za pomocą atrybutów:
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 interpretuje nazwy atrybutów tak samo, jakby były to klasy, czyli w
kontekście przestrzeni nazw i klauzul use
. Można by je więc
zapisać na przykład również tak:
use Nette\Application\Attributes;
class SomePresenter
{
#[Attributes\Persistent]
public int $page = 1;
#[\Nette\DI\Attributes\Inject]
public Facade $facade;
Klasa reprezentująca atrybut może istnieć lub nie. Jest jednak zdecydowanie lepiej, jeśli istnieje, ponieważ wtedy edytor może ją podpowiadać podczas pisania, analizator statyczny rozpozna literówki itp.
Składnia
Sprytne jest to, że PHP przed wersją 8 widzi atrybuty tylko jako komentarze, więc można ich używać również w kodzie, który ma działać w starszych wersjach.
Zapis pojedynczego atrybutu wygląda jak tworzenie instancji obiektu,
gdybyśmy pominęli operator new
. Czyli nazwa klasy, po której
mogą następować w nawiasach argumenty:
#[Column('string', 32, true, false)]#
protected $username;
I tu jest miejsce, gdzie można wykorzystać nową gorącą cechę PHP 8.0 – named arguments:
#[Column(
type: 'string',
length: 32,
unique: true,
nullable: false,
)]#
protected $username;
Każdy element może mieć wiele atrybutów, które można zapisać oddzielnie lub rozdzielone przecinkiem:
#[Inject]
#[Lazy]
public Foo $foo;
#[Inject, Lazy]
public Bar $bar;
Poniższy atrybut dotyczy wszystkich trzech właściwości:
#[Common]
private $a, $b, $c;
W wartościach domyślnych właściwości można używać prostych wyrażeń i stałych, które można obliczyć podczas kompilacji, i to samo dotyczy argumentów atrybutów:
#[
ScalarExpression(1 + 1),
ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
BitShift(4 >> 1, 4 << 1),
BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]
Niestety wartością argumentu nie może być kolejny atrybut, tj. nie można zagnieżdżać atrybutów. Na przykład poniższej adnotacji używanej w Doctrine nie można całkowicie bezpośrednio przekształcić w atrybuty:
/**
* @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
*/
Nie ma również odpowiednika atrybutu dla pliku phpDoc, czyli komentarza znajdującego się na początku pliku, który jest wykorzystywany np. przez Nette Tester.
Odczytywanie atrybutów
Jakie atrybuty mają poszczególne elementy, dowiemy się za pomocą
refleksji. Klasy refleksyjne dysponują nową metodą
getAttributes()
, która zwróci tablicę obiektów
ReflectionAttribute
.
use MyAttributes\Example;
#[Example('Witaj', 123)]
class Foo
{}
$reflection = new ReflectionClass(Foo::class);
foreach ($reflection->getAttributes() as $attribute) {
$attribute->getName(); // pełna nazwa atrybutu, np. MyAttributes\Example
$attribute->getArguments(); // ['Witaj', 123]
$attribute->newInstance(); // zwraca instancję new MyAttributes\Example('Witaj', 123)
}
Zwrócone atrybuty można filtrować parametrem, np.
$reflection->getAttributes(Example::class)
zwróci tylko
atrybuty Example
.
Klasy atrybutów
Klasa atrybutu MyAttributes\Example
nie musi istnieć. Jej
istnienia wymaga jedynie wywołanie metody newInstance()
, ponieważ
tworzy jej instancję. Napiszmy ją więc. Będzie to zupełnie zwyczajna klasa,
tylko musimy przy niej podać atrybut Attribute
(tj. z globalnej
przestrzeni nazw systemowej):
namespace MyAttributes;
use Attribute;
#[Attribute]
class Example
{
public function __construct(string $message, int $number)
{
...
}
}
Można ograniczyć, dla jakich elementów językowych będzie legalne użycie atrybutu. Na przykład tak, tylko dla klas i właściwości:
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
...
}
Dostępne są flagi TARGET_CLASS
, TARGET_FUNCTION
,
TARGET_METHOD
, TARGET_PROPERTY
,
TARGET_CLASS_CONSTANT
, TARGET_PARAMETER
oraz domyślna
TARGET_ALL
.
Ale uwaga, do weryfikacji poprawnego lub niepoprawnego użycia dochodzi
zaskakująco dopiero przy wywołaniu metody newInstance()
. Sam
kompilator tej kontroli nie przeprowadza.
Przyszłość z atrybutami
Dzięki atrybutom i nowym typom
komentarze do dokumentacji PHP po raz pierwszy w swojej historii staną się tak
naprawdę tylko komentarzami do dokumentów. PhpStorm już teraz posiada niestandardowe
atrybuty, które mogą zastąpić np. adnotację @deprecated
.
I można założyć, że ten atrybut będzie kiedyś w PHP domyślny. Podobnie
zastąpione zostaną inne adnotacje, takie jak @throws
itp.
Chociaż Nette używa adnotacji od swojej pierwszej wersji do oznaczania parametrów i komponentów persistentnych, do ich masowego wykorzystania nie doszło, ponieważ nie był to natywny konstrukt językowy, więc edytory ich nie podpowiadały i łatwo było w nich popełnić błąd. Chociaż dzisiaj rozwiązują to wtyczki do edytorów, naprawdę natywna droga, jaką przynoszą atrybuty, otwiera zupełnie nowe możliwości.
Nawiasem mówiąc, atrybuty uzyskały wyjątek w Nette Coding Standard,
który wymaga, aby nazwa klasy oprócz specyficzności (np.
Product
, InvalidValue
) zawierała również
ogólność (czyli ProductPresenter
,
InvalidValueException
). W przeciwnym razie nie byłoby przy użyciu
w kodzie jasne, co dokładnie klasa reprezentuje. W przypadku atrybutów jest to
wręcz niepożądane, więc klasa nazywa się Inject
zamiast
InjectAttribute
.
W ostatniej części przyjrzymy się, jakie nowe funkcje i klasy pojawiły się w PHP oraz przedstawimy Just in Time Compiler.
Aby przesłać komentarz, proszę się zalogować