PHP 8.0: Atrybuty (3/4)
PHP w wersji 8.0 jest właśnie wydawane. Jest ona pełna nowych rzeczy, jak żadna inna wersja wcześniej. Ich wprowadzenie zasługiwało na cztery osobne artykuły. W trzeciej części przyjrzymy się atrybutom.
Atrybuty zapewniają zupełnie nowy sposób pisania ustrukturyzowanych metadanych dla klas i wszystkich jej członków, a także funkcji, domknięć i ich parametrów. Do tej pory używano do tego celu komentarzy PhpDoc, ale ich składnia zawsze była tak luźna i niespójna, że nie dało się uruchomić maszynowego ich przetwarzania. Dlatego są one zastępowane przez atrybuty o ustalonej składni i wsparciu w klasach refleksyjnych.
Z tego powodu biblioteki, które do tej pory pobierały metadane poprzez
parsowanie komentarzy phpDoc będą mogły zastąpić je atrybutami. Przykładem
jest Nette, gdzie w najnowszych wersjach Application i DI można już używać
atrybutów Persistent
, CrossOrigin
i
Inject
zamiast adnotacji @persistent
,
@crossOrigin
i @inject
.
Kod wykorzystujący adnotacje:
/**
* @persistent(comp1, comp2)
*/
class SomePresenter
{
/** @persistent */
public $page = 1;
/** @inject */
public Facade $facade;
/**
* @crossOrigin
*/
public function handleSomething()
{
}
}
To samo z atrybutami:
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 ocenia nazwy atrybutów tak samo, jakby były one klasami, w kontekście
przestrzeni nazw i klauzul use
. Można by więc nawet napisać je
np. w następujący sposób:
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, ale nie musi istnieć. Ale zdecydowanie lepiej, jeśli istnieje, ponieważ wtedy edytor może sugerować go podczas pisania, analizator statyczny rozpoznaje 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ć także w kodzie, który powinien działać w starszych wersjach.
Składnia pojedynczego atrybutu wygląda jak tworzenie instancji obiektu,
jeśli pominiemy operator new
. Czyli nazwa klasy, a po niej
opcjonalne argumenty w nawiasach:
#[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 mogą być zapisane pojedynczo lub oddzielone 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;
Proste wyrażenia i stałe, które mogą być oceniane podczas kompilacji i są używane jako wartości domyślne dla właściwości, mogą być używane jako argumenty w atrybutach:
#[
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ść argumentu nie może być innym atrybutem, czyli atrybuty nie mogą być zagnieżdżone. Na przykład nie ma prostego sposobu, aby następująca adnotacja używana w Doctrine została przekształcona 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.
Odzwierciedlenie atrybutu
To, jakie atrybuty posiadają poszczególne elementy, można ustalić za
pomocą refleksji. Klasy refleksyjne posiadają nową metodę
getAttributes()
, która zwraca tablicę obiektów
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)
}
Zwracane atrybuty mogą być filtrowane przez parametr, np.
$reflection->getAttributes(Example::class)
zwraca tylko atrybuty
Example
.
Klasy atrybutów
Klasa atrybutów MyAttributes\Example
nie może istnieć.
Dopiero wywołanie metody newInstance()
wymaga jej istnienia, bo
ją instancjonuje. Napiszmy więc ją. Będzie to zupełnie zwykła klasa, tylko
musimy dostarczyć atrybut Attribute
(czyli z globalnej
przestrzeni nazw systemu):
namespace MyAttributes;
use Attribute;
#[Attribute]
class Example
{
public function __construct(string $message, int $number)
{
...
}
}
Można ograniczyć, dla jakich elementów języka użycie atrybutu będzie dozwolone. Na przykład 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
i domyślna
TARGET_ALL
.
Ale uwaga, sprawdzenie poprawności lub niepoprawności użycia następuje
zaskakująco dopiero po wywołaniu metody newInstance()
. Sam
kompilator nie wykonuje tego sprawdzenia.
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 od pierwszej wersji używało adnotacji do wskazywania trwałych parametrów i komponentów, nie były one masowo używane, ponieważ nie były konstrukcją rodzimego języka, więc redaktorzy nie sugerowali się nimi i łatwo było popełnić błąd. Nawet jeśli jest to już rozwiązywane przez wtyczki do edytorów, to naprawdę natywny sposób, który przynoszą atrybuty, otwiera zupełnie nowe możliwości.
Przy okazji atrybuty zyskały wyjątek w Standardzie Kodowania Nette, który
wymaga, aby nazwa klasy oprócz specyfiki (np. Product
,
InvalidValue
) zawierała także ogólność (czyli
ProductPresenter
, InvalidValueException
). W przeciwnym
razie, po użyciu w kodzie, nie byłoby jasne, co dokładnie reprezentuje dana
klasa. W przypadku atrybutów nie jest to pożądane, dlatego klasa nazywa się
Inject
zamiast InjectAttribute
.
*W ostatniej części przyjrzymy się, jakie nowe funkcje i klasy pojawiły się w PHP oraz przedstawimy kompilator Just in Time.
Aby przesłać komentarz, proszę się zalogować