• Время чтения ~4 мин
  • 27.04.2023

Атрибуты PHP были добавлены в версии 8.0 языка, и это было неправильное название для многих разработчиков. Каковы их преимущества и как я могу ими воспользоваться?

Это вопрос, который я задаю себе с момента их выпуска, и только недавно я нашел для них вариант использования. Работая над проектом, требующим доступа к API, я решил использовать Laravel Breeze и добавить оболочку вокруг токенов API Sanctums вместо Jetstream. Это привело меня к выяснению того, как наилучшим образом использовать сами токены.

Если вы использовали Laravel Jetstream раньше, вы будете знать, что регистрируете разрешения и возможности токенов у поставщика услуг. Это приемлемый подход, если у вас упрощенный API. Тем не менее, мои потребности были более сложными, чем это, но не настолько сложными, чтобы нужно было настраивать OAuth.

Вместо этого я подумал, что буду использовать нативную enum структуру PHP, один из моих распространенных подходов при хранении ролей пользователей. Но перечисления недостаточно подробны, что создает проблему. Затем я наткнулся на фантастический и вдохновляющий учебник Роба Фонески. Он писал о том, как можно расширить перечисления PHP с помощью атрибутов. Его вариант использования отличался от моего, но вау - у меня наконец-то открылись глаза на вариант использования!

Мне нужно было создать набор разрешений, которые позволяли бы созданным пользователем токенам API иметь определенные возможности. Тем не менее, я хотел, чтобы пользователь также понимал разрешения, которые он устанавливает. Мой первый шаг состоял в том, чтобы создать базовое перечисление:

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

Эти два типа разрешений имеют четкое различие. Один должен уметь делать все, в то время как другой имеет более ограниченный доступ. Здесь я оглянулся на учебник Робса и реализовал атрибут description.

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

Я хотел, чтобы мои атрибуты были неизменными, чтобы ничто не могло их изменить. Так что класс только для чтения имел здесь большой смысл, не то чтобы мне нужно оправдание ...

Теперь все, что мне нужно было сделать, это добавить атрибут в мое перечисление:

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';
}

Это дало информацию о каждом разрешении, которую я хотел, чтобы пользователь мог понять. Однако тогда я столкнулся с другой проблемой. Как я могу хранить способности? Я знал, что перечисления допускают только строки или целые числа в качестве падежей, так что я мог сделать?

Опять же, я нашел свой ответ в атрибутах - неожиданно. Если атрибут можно использовать для добавления описания, его также можно использовать для добавления других вещей. Поэтому я создал еще один атрибут под названием Abilities, который будет принимать массив строк, чтобы иметь свободный подход к их перечислению.

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

Теперь все, что мне нужно было сделать, это добавить это в мое перечисление, и я мог установить токен в перечисление и использовать отражение, чтобы извлечь способности при сохранении в базе данных.

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';
}

Вот что у меня получилось в итоге. Я хотел, чтобы ссылочный ключ имел более удобную строку для ссылки. Теперь я мог следовать руководству Робса и реализовать способ доступа к этим атрибутам.

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;
    }
}

Простая черта, позволяющая мне получить доступ ко всем атрибутам, которые я хотел. Поскольку мое приложение использовало Inertia, все, что мне нужно было сделать, это передать ресурс в промежуточномHandlesInertia программном обеспечении, чтобы мой пользовательский интерфейс мог получить доступ к этим разрешениям везде в деталях. Я решил создать для этого ресурс API, чтобы я мог последовательно обрабатывать форматирование.

/**
 * @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),
        ];
    }
}

Наконец-то я нашел вариант использования атрибутов, создав то, что я считаю отличным способом регистрации этих данных в вашем приложении.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

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...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297