Расширение перечислений PHP 8.1 с помощью атрибутов

Источник: «Extending PHP 8.1 enums with attributes»
Атрибуты PHP, заимствованные из концепции аннотаций в других языках, могут добавить вашим перечислениям мощную функциональность.

С релизом PHP 8.1 язык получил встроенную поддержку перечислений. Перечисления (Enums) — это типобезопасный, удобочитаемый и эффективный способ инкапсулирования небольшого набора возможных значений, которые может принимать поле в вашей модели данных. Использование классов вместо перечислений базы данных обеспечивает большую гибкость, если вам нужно будет расширить список в будущем.

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

namespace App\Enums;

enum UserRole: string
{
case Admin = 'admin';
case TeamAdmin = 'team_admin';
case Support = 'support';
case Basic = 'basic';
}

В вашей модели данных Laravel также поддерживает перечисления, приводя их к значению, если вы определяете его в массиве приведений.

/**
* Атрибуты, которые должны быть приведены.
*
* @var array<string,string|class-string>
*/

protected $casts = [
'role' => UserRole::class,
];

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

Очень практичное использование перечислений заключается в создании значений для раскрывающегося списка в HTML.

<select name=”roles”>
@foreach(UserRole::cases() as $role)
<option value="{{ $role->value }}">{{ $role->name }}</option>
@endforeach
</select>

На первый взгляд, в приведённом выше примере нет ничего плохого, пока вы не посмотрите на видимое имя каждой опции в раскрывающемся списке. Admin и TeamAdmin — отличные имена переменных, но Administrator и Team Administrator лучше выглядят в пользовательском интерфейсе, чтобы было совершенно ясно, какая роль принадлежит человеку, управляющему ролями пользователей.

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

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

Во-первых, нам нужно создать атрибут Description.

namespace App\Enums\Attributes;

use Attribute;

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

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

namespace App\Enums;

enum UserRole: string
{
#[Description('Administrator')]
case Admin = 'admin';

#[Description('Team Administrator')]
case TeamAdmin = 'team_admin';

case Support = 'support';
case basic = 'basic';
}

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

namespace App\Enums\Concerns;

use Illuminate\Support\Str;
use ReflectionClassConstant;
use App\Enums\Attributes\Description;

trait GetsAttributes
{
/**
* @param self $enum
*/

private static function getDescription(self $enum): string
{
$ref = new ReflectionClassConstant(self::class, $enum->name);
$classAttributes = $ref->getAttributes(Description::class);

if (count($classAttributes) === 0) {
return Str::headline($enum->value);
}

return $classAttributes[0]->newInstance()->description;
}
}

Если мы разберём этот метод, то первые две строки используют отражения для получения атрибутов перечисления. Поскольку не каждое перечисление может иметь атрибут Description, мы настраиваем запасной вариант, для преобразования значения (или имени) этого перечисления в наше описание (description).

Наконец, мы извлекаем значение описания из атрибутов перечисления. Мы можем добавить ещё один метод к трейту, чтобы справиться с этой задачей.

/**
* @return array<string,string>
*/

public static function asSelectArray(): array
{
/** @var array<string,string> $values */
$values = collect(self::cases())
->map(function ($enum) {
return [
'name' => self::getDescription($enum),
'value' => $enum->value,
];
})->toArray();

return $values;
}

Теперь в HTML мы можем просто изменить метод, вызываемый в классе перечисления enum.

<select name=”roles”>
@foreach(UserRoles::asSelectArray() as $role)
<option value=”{{ $role->value }}”>{{ $role->name }}</option>
@endforeach
</select>

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

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

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

Новое в Symfony 6.3 — Преобразование целевых значений

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

Laravel: Девять типичных ошибок новичков