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

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

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

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

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

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

/**
* The attributes that should be cast.
*
* @var array|class-string>
*/
protected $casts = [
   'role' => UserRole::class,
];

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

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

<select name=&rdquo;roles&rdquo;>
   @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, настраиваем запасной вариант для преобразования значения (или имени) этого перечисления в качестве нашего описания.

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

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

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

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

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

Оригинал статьи: https://laravel-news.com/extending-php-enums-with-attributes

Комментарии
Nikolay
Nikolay
06.02.2024 в 09:47
Можно же сильно проще все сделать. Enum поддерживает методы и может реализовывать интерфейсы. Пишем интерфейс: <?php declare(strict_types=1); namespace App\Enums; interface Descriptable { function description(): string; } И реализуем его в перечислении: <?php declare(strict_types=1); namespace App\Enums; enum UserMode: string implements Descriptable { case CUSTOMER = 'customer'; case CONTRACTOR = 'contractor'; public function description(): string { return match ($this) { self::CUSTOMER => 'Заказчик', self::CONTRACTOR => 'Исполнитель', }; } } И все готово. <?php //... UserMode::CUSTOMER->description(); // вернёт "Заказчик" Все описано в доке: https://www.php.net/manual/ru/language.enumerations.methods.php