CONST умер! Да здравствует ENUM!

CONST умер! Да здравствует ENUM!

Леонид Суматохин

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

class Status {
 const ACTIVE = 1;
 const INACTIVE = 0;
}

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

  • Ошибки в значениях: легко случайно присвоить неправильный числовой идентификатор или строку;
  • Отсутствие встроенного контроля типов: компилятор или интерпретатор не могли гарантировать правильность передаваемых значений;

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

if ($status === Status::CANCELLED) { ... }

В данном случае PHP об ошибке не скажет, но и нужного результата мы не получим: константа CANCELLED просто не определена. 

Всё это и привело к появлению стандартизированного способа объявления перечислений.

Как всё начиналось

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

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

  • Будет ли новая конструкция наследоваться от базового класса?
  • Какие операции будут разрешены над экземплярами перечислений?
  • Нужно ли поддерживать возможность кастинга между разными типами?

В итоге полноценная реализация перечислений состоялась в PHP 8.1, выпущенном осенью 2021 года. Эта версия добавила новый специальный тип данных — enum.

Теперь вместо обычных констант стали использоваться настоящие объекты, обладающие строгими правилами безопасности и чёткими возможностями проверки типов:

enum Status {     
 case Active;     
 case Inactive;
}

В PHP перечисления относятся к виду объектов. Сами перечисления — классы, а каждый возможный случай перечисления — объект одного экземпляра этого класса. Поэтому варианты перечисления — допустимые объекты, которые будут работать там же, где работают объекты, включая проверки типов.

Что умеет ENUM

Перечисления могут быть обычные/чистые:

enum Status {     
 case Active;     
 case Inactive;
}

А могут быть типизированными:

enum Status: string {     
 case Active = ‘active’;     
 case Inactive = ‘inactive’;
}
Причём перечисления могут быть или только чистыми или только типизированными, а их пересечение не допускается.

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

Значения в рамках одного enum могут быть или только integer или только string

Так же типизированные enum имеют два специфичных метода:

  • from(int|string): self возьмёт скаляр и вернёт вариант перечисления, которому он принадлежит.
  • tryFrom(int|string): ?self возьмёт скаляр и вернёт вариант перечисления, которому он принадлежит. Если вариант, который соответствует варианту перечисления, не найден, метод вернёт null. 
А ещё в перечислениях разрешено объявлять публичные, приватные и защищённые методы. Они могут реализовывать интерфейсы, разрешено использование констант и трейтов, вот наследование перечислений не разрешено.

Благодаря встроенному методу cases, enum удобно перебирать:

foreach (Status::cases() as $status) {
   echo $status->value;
}

Например для построения выпадающих списков.

Так же enum можно использовать для подсказки типов в функциях:

function setStatus(Status $status) {
    // сохранение статуса в базу данных
}

Благодаря этому аргумент метода ожидает опцию только из перечисления Status.

И ещё один не маловажный аспект перечислений – это уникальность их значений. 

----------------

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


В качестве послесловия

Я при разработке к enum добавляю удобный trait, который прокачивает enum ещё немного:

interface EnumArrayableInterface
 {
     public static function names(): array;
     public static function values(): array;
     public static function array(): array;
 }
trait EnumAsArray
 {
     public static function names(): array
     {
         return array_column(self::cases(), 'name');
    }

     public static function values(): array
     {
         return array_column(self::cases(), 'value');
    }

     public static function array(): array
     {
         return array_combine(self::names(), self::values());
    }
 }
 
enum ReportStatus: string implements Arrayable
 {
     use EnumAsArray;
     case NEW = 'new';
     case PROCESSED = 'processed';
     case DONE = 'done';
 }

Источники: Документация, Medium

Report Page