• Czas czytania ~8 min
  • 04.08.2023

Korzystasz z przestarzałej aplikacji Laravel? Uzyskaj natychmiastowe, zautomatyzowane aktualizacje Laravel dzięki Laravel Shift

Od PHP 8 będziemy mogli używać atrybutów. Celem tych atrybutów, znanych również jako adnotacje w wielu innych językach, jest dodawanie metadanych do klas, metod, zmiennych i innych elementów; w uporządkowany sposób.

Koncepcja atrybutów nie jest wcale nowa, od lat używamy docblocków do symulacji ich zachowania. Jednak po dodaniu atrybutów mamy teraz pierwszorzędnego obywatela w języku, który reprezentuje tego rodzaju metadane, zamiast ręcznie analizować bloki dokumentów.

Jak więc wyglądają? Jak tworzymy atrybuty niestandardowe? Czy są jakieś zastrzeżenia? To są pytania, na które odpowiemy w tym poście. Zanurzmy się!

# Podsumowanie

Po pierwsze, oto, jak atrybut wyglądałby na wolności:

use \Support\Attributes\ListensTo;
class ProductSubscriber
{
    #[ListensTo(ProductCreated::class)]
    public function onProductCreated(ProductCreated $event) { /* … */ }
    #[ListensTo(ProductDeleted::class)]
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

Pokażę inne przykłady w dalszej części tego postu, ale myślę, że przykład subskrybentów wydarzeń jest dobry do wyjaśnienia użycia atrybutów na początku.

Również tak, wiem, składnia może nie być taka, jak sobie życzyłeś lub na co liczyłeś. Być może wolałeś @, lub @:, lub docblocks albo, ... Jest tu jednak po to, aby pozostać, więc lepiej nauczmy się sobie z tym radzić. Jedyną rzeczą, o której warto wspomnieć w składni, jest to, że wszystkie opcje zostały omówione i istnieją bardzo dobre powody, dla których wybrano tę składnię. Możesz przeczytać całą dyskusję na temat RFC na liście wewnętrznej.

Biorąc to pod uwagę, skupmy się na fajnych rzeczach: jak to ListensTo działałoby pod maską?

Po pierwsze, atrybuty niestandardowe są prostymi klasami, opatrzonymi adnotacjami za pomocą atrybutu#[Attribute]; ta baza Attribute była wywoływana PhpAttribute w oryginalnym RFC, ale później została zmieniona na inne RFC.

Oto, jak by to wyglądało:

#[Attribute]
class ListensTo
{
    public string $event;
    public function __construct(string $event)
    {
        $this->event = $event;
    }
}

To wszystko - całkiem proste, prawda? Pamiętaj o celu atrybutów: mają one na celu dodawanie metadanych do klas i metod, nic więcej. Nie powinny – i nie mogą – być używane na przykład do sprawdzania poprawności danych wejściowych argumentów. Innymi słowy: nie miałbyś dostępu do parametrów przekazywanych do metody w jej atrybutach. Istniało poprzednie RFC, które zezwalało na takie zachowanie, ale to RFC specjalnie upraszczało sprawę.

Wracając do przykładu subskrybenta wydarzenia: nadal musimy czytać metadane i rejestrować naszych subskrybentów gdzieś na miejscu. Pochodzę ze środowiska Laravel, skorzystałbym z usługodawcy jako miejsca, w którym mógłbym to zrobić, ale nie krępuj się wymyślić innych rozwiązań.

Oto nudna konfiguracja standardowa, aby zapewnić trochę kontekstu:

class EventServiceProvider extends ServiceProvider
{
    // In real life scenarios, 
    //  we'd automatically resolve and cache all subscribers
    //  instead of using a manual array.
    private array $subscribers = [
        ProductSubscriber::class,
    ];
    public function register(): void
    {
        // The event dispatcher is resolved from the container
        $eventDispatcher = $this->app->make(EventDispatcher::class);
        foreach ($this->subscribers as $subscriber) {
            // We'll resolve all listeners registered 
            //  in the subscriber class,
            //  and add them to the dispatcher.
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

Zauważ, że jeśli [$event, $listener] składnia jest ci nieznana, możesz przyspieszyć ją w moim poście o destrukcji tablicy.

Teraz spójrzmy na resolveListeners, gdzie dzieje się magia.

private function resolveListeners(string $subscriberClass): array
{
    $reflectionClass = new ReflectionClass($subscriberClass);
    $listeners = [];
    foreach ($reflectionClass->getMethods() as $method) {
        $attributes = $method->getAttributes(ListensTo::class);
        
        foreach ($attributes as $attribute) {
            $listener = $attribute->newInstance();
            
            $listeners[] = [
                // The event that's configured on the attribute
                $listener->event,
    
                // The listener for this event 
                [$subscriberClass, $method->getName()],
            ];
        }
    }
    return $listeners;
}

Widać, że łatwiej jest odczytać metadane w ten sposób w porównaniu do analizowania ciągów docblock. Są jednak dwie zawiłości, którym warto się przyjrzeć.

Najpierw jest wezwanie$attribute->newInstance(). Jest to miejsce, w którym tworzona jest instancja naszej klasy atrybutów niestandardowych. Pobiera parametry wymienione w definicji atrybutu w naszej klasie subskrybenta i przekazuje je do konstruktora.

Oznacza to, że technicznie nie musisz nawet konstruować atrybutu niestandardowego. Możesz zadzwonić $attribute->getArguments() bezpośrednio. Co więcej, tworzenie instancji klasy oznacza, że masz elastyczność konstruktora do wprowadzania parse w dowolny sposób. W sumie powiedziałbym, że dobrze byłoby zawsze tworzyć instancję atrybutu za pomocą newInstance().

Drugą rzeczą, o której warto wspomnieć, jest użycie ReflectionMethod::getAttributes()funkcji , która zwraca wszystkie atrybuty metody. Możesz przekazać do niego dwa argumenty, aby filtrować jego dane wyjściowe.

Aby zrozumieć to filtrowanie, musisz najpierw wiedzieć o atrybutach. Mogło to być dla ciebie oczywiste, ale i tak chciałem o tym wspomnieć bardzo szybko: możliwe jest dodanie kilku atrybutów do tej samej metody, klasy, właściwości lub stałej.

Możesz na przykład zrobić to:

#[
    Route(Http::POST, '/products/create'),
    Autowire,
]
class ProductsCreateController
{
    public function __invoke() { /* … */ }
}

Mając to na uwadze, jasne jest, dlaczego Reflection*::getAttributes() zwraca tablicę, więc przyjrzyjmy się, jak można filtrować jej dane wyjściowe.

Załóżmy, że analizujesz trasy kontrolera, interesuje Route Cię tylko atrybut. Możesz łatwo przekazać tę klasę jako filtr:

$attributes = $reflectionClass->getAttributes(Route::class);

Drugi parametr zmienia sposób filtrowania. Możesz przekazać , ReflectionAttribute::IS_INSTANCEOFco zwróci wszystkie atrybuty implementujące dany interfejs.

Załóżmy na przykład, że analizujesz definicje kontenerów, które opierają się na kilku atrybutach, możesz zrobić coś takiego:

$attributes = $reflectionClass->getAttributes(
    ContainerAttribute::class, 
    ReflectionAttribute::IS_INSTANCEOF
);

To ładny skrót wbudowany w rdzeń.

Zauważyłeś tpyo? Możesz przesłać żądanie ściągnięcia, aby to naprawić. Jeśli chcesz być na bieżąco z tym, co dzieje się na tym blogu, możesz śledzić mnie na Twitterze lub zapisać się do mojego biuletynu:

# Teoria

techniczna Teraz, gdy masz już pojęcie o tym, jak atrybuty działają w praktyce, nadszedł czas na więcej teorii, upewniając się, że dokładnie je rozumiesz. Po pierwsze, wspomniałem o tym pokrótce wcześniej, atrybuty można dodać w kilku miejscach.

W klasach, a także w zajęciach anonimowych;

#[ClassAttribute]
class MyClass { /* … */ }
$object = new #[ObjectAttribute] class () { /* … */ };

Właściwości i stałe;

#[PropertyAttribute]
public int $foo;
#[ConstAttribute]
public const BAR = 1;

Metody i funkcje;

#[MethodAttribute]
public function doSomething(): void { /* … */ }
#[FunctionAttribute]
function foo() { /* … */ }

Jak również zamknięcia;

$closure = #[ClosureAttribute] fn() => /* … */;

Parametry metody i funkcji;

function foo(#[ArgumentAttribute] $bar) { /* … */ }

Mogą być deklarowane przed lub po docblockach;

/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }

I nie może przyjmować żadnego, jednego lub kilku argumentów, które są zdefiniowane przez konstruktor atrybutu:

#[Listens(ProductCreatedEvent::class)]
#[Autowire]
#[Route(Http::POST, '/products/create')]

Jeśli chodzi o dozwolone parametry, które możesz przekazać do atrybutu, już widziałeś, że stałe klasy, ::class nazwy i typy skalarne są dozwolone. Jest jednak trochę więcej do powiedzenia na ten temat: atrybuty akceptują tylko wyrażenia stałe jako argumenty wejściowe.

Oznacza to, że dozwolone są wyrażenia skalarne — nawet przesunięcia bitowe — a także ::classstałe, tablice i rozpakowywanie tablic, wyrażenia logiczne i operator łączenia wartości null. Listę wszystkiego, co jest dozwolone jako wyrażenie stałe, można znaleźć w kodzie źródłowym.

#[AttributeWithScalarExpression(1 + 1)]
#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
#[AttributeWithClassConstant(Http::POST)]
#[AttributeWithBitShift(4 >> 1, 4 << 1)]

# Konfiguracja

atrybutów Domyślnie atrybuty można dodawać w kilku miejscach, jak wymieniono powyżej. Możliwe jest jednak skonfigurowanie ich tak, aby mogły być używane tylko w określonych miejscach. Na przykład możesz zrobić to tak, że ClassAttribute może być używany tylko na zajęciach i nigdzie indziej. Włączenie tego zachowania odbywa się poprzez przekazanie flagi do atrybutu w klasie atrybutuAttribute.

Wygląda to tak:Dostępne są następujące flagi:

#[Attribute(Attribute::TARGET_CLASS)]
class ClassAttribute
{
}

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

Są to flagi masek bitowych, więc można je łączyć za pomocą operacji binarnej OR.

#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}

Kolejna flaga konfiguracji dotyczy powtarzalności. Domyślnie ten sam atrybut nie może być stosowany dwa razy, chyba że jest specjalnie oznaczony jako powtarzalny. Odbywa się to w taki sam sposób, jak konfiguracja docelowa, z flagą bitową.

#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}

Należy pamiętać, że wszystkie te flagi są sprawdzane tylko podczas wywoływania $attribute->newInstance(), a nie wcześniej.

# Wbudowane atrybuty Po zaakceptowaniu podstawowego RFC pojawiły się nowe możliwości dodania wbudowanych atrybutów

do rdzenia. Jednym z takich przykładów jest atrybut, a popularnym przykładem był #[Jit] atrybut - jeśli nie jesteś pewien, o czym jest ten ostatni, #[Deprecated] możesz przeczytać mój post o tym, czym jest JIT.

Jestem pewien, że w przyszłości zobaczymy coraz więcej wbudowanych atrybutów.

Na koniec, dla tych, którzy martwią się generykami: składnia nie będzie z nimi kolidować, jeśli kiedykolwiek zostaną dodane w PHP, więc jesteśmy bezpieczni!



Mam już kilka przypadków użycia atrybutów, a co z tobą? Jeśli masz jakieś przemyślenia na temat tej niesamowitej nowej funkcji w PHP 8, możesz skontaktować się ze mną na Twitterze lub przez e-mail, lub możemy omówić to na Reddit.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297