• Czas czytania ~4 min
  • 27.04.2023

Atrybuty PHP zostały dodane w wersji 8.0 języka i było to mylące dla wielu programistów. Jakie są ich zalety i jak mogę z nich korzystać?

To jest pytanie, które zadaję sobie od czasu ich wydania i dopiero niedawno znalazłem dla nich przypadek użycia. Pracując nad projektem wymagającym dostępu do API, postanowiłem użyć Laravel Breeze i dodać opakowanie wokół tokenów API Sanctums zamiast Jetstream. To doprowadziło mnie na ścieżkę zastanawiania się, jak najlepiej wykorzystać same żetony.

Jeśli korzystałeś wcześniej z Laravel Jetstream, będziesz wiedział, że rejestrujesz uprawnienia i możliwości tokenów u usługodawcy. Jest to akceptowalne podejście, jeśli masz uproszczony interfejs API. Jednak moje potrzeby były bardziej złożone niż to - ale nie na tyle złożone, aby trzeba było skonfigurować OAuth.

Zamiast tego pomyślałem, że wykorzystuję natywną enum strukturę PHP, jedno z moich powszechnych podejść do przechowywania ról użytkowników. Ale wyliczenia nie są wystarczająco szczegółowe, co stanowi problem. Następnie natknąłem się na fantastyczny i inspirujący samouczek autorstwa Roba Fonesca. Napisał o tym, jak można rozszerzyć wyliczenia PHP za pomocą atrybutów. Jego przypadek użycia różnił się od mojego, ale wow - moje oczy w końcu otworzyły się na przypadek użycia!

Musiałem utworzyć zestaw uprawnień, które umożliwiłyby tokenom interfejsu API utworzonym przez użytkowników posiadanie określonych możliwości. Chciałem jednak umożliwić użytkownikowi zrozumienie uprawnień, które ustawia. Moim pierwszym krokiem było stworzenie podstawowego wyliczenia:

enum Permission: string
{
    case ADMIN = 'ADMIN';
 
    case EDITOR = 'EDITOR';
}

Te dwa typy uprawnień mają wyraźne rozróżnienie. Jeden powinien być w stanie zrobić wszystko - podczas gdy drugi ma bardziej ograniczony dostęp. W tym miejscu spojrzałem wstecz na samouczek Robsa i zaimplementowałem atrybut description.

use Attribute;
 
#[Attribute]
final readonly class Description
{
    public function __construct(
        public string $description,
    ) {}
}

Chciałem, aby moje atrybuty były niezmienne, aby nic nie mogło ich zmienić. Tak więc klasa tylko do odczytu miała tu wiele sensu, a nie żebym potrzebował wymówki ...

Teraz wszystko, co musiałem zrobić, to dodać atrybut do mojego enum:To dało informacje o każdym uprawnieniu, które chciałem,

enum Permission: string
{
    #[Description('Admin users can perform any action.')]
    case ADMIN = 'ADMIN';
 
    #[Description('Editor users have the ability to read, and update.')]
    case EDITOR = 'EDITOR';
}

aby użytkownik mógł zrozumieć. Jednak wtedy stanąłem przed innym problemem. Jak mogę przechowywać umiejętności? Wiedziałem, że wyliczenia dopuszczają tylko ciągi lub liczby całkowite jako przypadki, więc co mogłem zrobić?

Ponownie znalazłem odpowiedź w atrybutach - nieoczekiwanie. Jeśli atrybut może być użyty do dodania opisu, może być również użyty do dodania innych rzeczy. Stworzyłem więc kolejny atrybut, o nazwie Abilities, który przyjmowałby szereg ciągów, aby mieć swobodne podejście do ich wymieniania.

#[Attribute]
final readonly class Abilities
{
    public function __construct(
        public array $abilities,
    ) {}
}

Teraz wszystko, co musiałem zrobić, to dodać to do mojego wyliczenia i mogłem ustawić token na wyliczenie i użyć odbicia, aby wyciągnąć umiejętności podczas zapisywania w bazie danych.

enum Permission: string
{
    #[Key('admin')]
    #[Description('Admin users can perform any action.')]
    #[Abilities(['create','read','update','delete'])]
    case ADMIN = 'ADMIN';
 
    #[Key('editor')]
    #[Description('Editor users have the ability to read, and update.')]
    #[Abilities(['read','update'])]
    case EDITOR = 'EDITOR';
}

Oto, co skończyłem. Chciałem, aby klucz referencyjny miał ładniejszy ciąg znaków do odniesienia. Teraz mogłem śledzić samouczek Robsa i zaimplementować sposób dostępu do tych atrybutów.

trait CanAccessAttributes
{
    public static function abilities(BackedEnum $enum): array
    {
        $reflection = new ReflectionClassConstant(
            class: self::class,
            constant: $enum->name,
        );
 
        $attributes = $reflection->getAttributes(
            name: Abilities::class,
        );
 
        if (0 === count($attributes)) {
            return [Str::headline(
                value: strval($enum->value)
            )];
        }
 
        return $attributes[0]->newInstance()->abilities;
    }
 
    public static function key(BackedEnum $enum): string
    {
        $reflection = new ReflectionClassConstant(
            class: self::class,
            constant: $enum->name,
        );
 
        $attributes = $reflection->getAttributes(
            name: Key::class,
        );
 
        if (0 === count($attributes)) {
            return Str::headline(
                value: $enum->value
            );
        }
 
        return $attributes[0]->newInstance()->key;
    }
 
    public static function description(BackedEnum $enum): string
    {
        $reflection = new ReflectionClassConstant(
            class: self::class,
            constant: $enum->name,
        );
 
        $attributes = $reflection->getAttributes(
            name: Description::class,
        );
 
        if (0 === count($attributes)) {
            return Str::headline(
                value: $enum->value
            );
        }
 
        return $attributes[0]->newInstance()->description;
    }
}

Prosta cecha, która pozwala mi uzyskać dostęp do wszystkich atrybutów, które chciałem. Ponieważ moja aplikacja korzystała z Inertii, wszystko, co musiałem zrobić, to przekazać zasób w oprogramowaniuHandlesInertia pośredniczącym, aby mój interfejs użytkownika mógł uzyskać dostęp do tych uprawnień wszędzie w szczegółach. Postanowiłem utworzyć w tym celu zasób API, aby móc konsekwentnie obsługiwać formatowanie.

/**
 * @property-read Permission $resource
 */
final class PermissionResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'key' => $this->resource->key($this->resource),
            'name' => $this->resource->name,
            'value' => $this->resource->value,
            'description' => $this->resource->description($this->resource),
            'abilities' => $this->resource->abilities($this->resource),
        ];
    }
}

W końcu znalazłem przypadek użycia atrybutów, jednocześnie budując to, co uważam za świetny sposób na zarejestrowanie tych danych w aplikacji.

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