Использование PHP Атрибутов для добавления значения

Источник: «Using attributes to add value»
PHP Атрибуты были добавлены в PHP 8.0, и для многих разработчиков это было неверным названием. Каковы их преимущества и как их использовать?

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

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

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

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

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

case EDITOR = 'EDITOR';
}

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

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

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

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

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

Теперь всё что нужно сделать, это добавить их в моё перечисление. И я мог установить токен в перечисление и использовать отражение, чтобы извлечь возможности/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, всё, что мне нужно было сделать — передать ресурс через middleware HandlesInertia, чтобы мой UI мог получить доступ к этим разрешениям везде и в деталях. Я решил создать API Resource, чтобы можно было последовательно обрабатывать форматирование.

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

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

Дополнительные материалы

Предыдущая Статья

10 Функций JavaScript и TypeScript, которых я избегаю

Следующая Статья

JavaScript: Что такое API и как с ним работать