Перечисления в PHP 8.1

PHP
Перечисления в PHP 8.1
Перевод статьи «PHP 8.1: Enums»

Они наконец-то появятся: поддержка перечислений будет добавлена в PHP 8.1! Пост посвящён более подробному рассмотрению нового функционала.

Начнём с того, как выглядят перечисления:

1enum Status
2{
3 case DRAFT;
4 case PUBLISHED;
5 case ARCHIVED;
6}

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

1class BlogPost
2{
3 public function __construct(
4 public Status $status,
5 ) {}
6}

В примере выше создание BlogPost и передача в него перечисления выглядит так:

1$post = new BlogPost(Status::DRAFT);

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

Методы перечислений

Перечисления могут определять методы, как и обычные классы. Это очень удобно, особенно в сочетании с оператором match:

1enum Status
2{
3 case DRAFT;
4 case PUBLISHED;
5 case ARCHIVED;
6 
7 public function color(): string
8 {
9 return match($this)
10 {
11 Status::DRAFT => 'grey',
12 Status::PUBLISHED => 'green',
13 Status::ARCHIVED => 'red',
14 };
15 }
16}

Методы можно использовать так:

1$status = Status::ARCHIVED;
2 
3$status->color(); // 'red'

Также можно использовать статичные методы:

1enum Status
2{
3 // …
4 
5 public static function make(): Status
6 {
7 // …
8 }
9}

И использовать в перечислениях self:

1enum Status
2{
3 // …
4 
5 public function color(): string
6 {
7 return match($this)
8 {
9 self::DRAFT => 'grey',
10 self::PUBLISHED => 'green',
11 self::ARCHIVED => 'red',
12 };
13 }
14}

Перечисления и интерфейсы

Также как и классы, перечисления могут реализовывать интерфейсы:

1interface HasColor
2{
3 public function color(): string;
4}
5 
6enum Status implements HasColor
7{
8 case DRAFT;
9 case PUBLISHED;
10 case ARCHIVED;
11 
12 public function color(): string { /* … */ }
13}

Значения перечислений

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

1enum Status: string
2{
3 case DRAFT = 'draft';
4 case PUBLISHED = 'published';
5 case ARCHIVED = 'archived';
6}

Обратите внимание на объявление типа в определении перечисления. Он указывает на то, что все значения перечисления относятся к указанному типу. Вы также можете сделать его int. В качестве типа можно использовать только int или string.

1enum Status: int
2{
3 case DRAFT = 1;
4 case PUBLISHED = 2;
5 case ARCHIVED = 3;
6}

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

Типизированные перечисления с интерфейсами

Если вы используете типизированные перечисления совместно с интерфейсами, тип должен стоять сразу после имени перечисления, перед ключевым словом implements.

1enum Status: string implements HasColor
2{
3 case DRAFT = 'draft';
4 case PUBLISHED = 'published';
5 case ARCHIVED = 'archived';
6 
7 // …
8}

Сериализация типизированных перечислений

Если вы присваиваете значения вариантам перечислений, вам, вероятно, понадобится способ их сериализации и десериализации. Под сериализацией подразумевается, что вам нужен способ получить значение перечисления. Это делается с помощью общедоступного readonly-свойства:

1$value = Status::PUBLISHED->value; // 2

Для получения перечисления по значению можно использовать метод Enum::from:

1$status = Status::from(2); // Status::PUBLISHED

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

1$status = Status::from('unknown'); // ValueError
2$status = Status::tryFrom('unknown'); // null

Обратите внимание, вы можете использовать встроенные функции serialize и unserialize при работе c перечислениями. Кроме того, вы можете использовать json_encode в сочетании с типизированными перечислениями, результатом выполнения функции будет значение перечисления. Поведение можно переопределить, реализовав JsonSerializable.

Вывод вариантов перечисления

Чтобы получить список всех доступных вариантов перечисления, воспользуйтесь статичным методом Enum::cases():

1Status::cases();
2 
3/* [
4Status::DRAFT,
5Status::PUBLISHED,
6Status::ARCHIVED
7] */

Обратите внимание, что в массиве содержатся объекты перечислений:

1array_map(
2fn(Status $status) => $status->color(),
3Status::cases()
4);

Перечисления — это объекты

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

1$statusA = Status::PENDING;
2$statusB = Status::PENDING;
3$statusC = Status::ARCHIVED;
4 
5$statusA === $statusB; // true
6$statusA === $statusC; // false
7$statusC instanceof Status; // true

Перечисления как ключи массива

Поскольку перечисления являются объектами, в настоящее время невозможно использовать их в качестве ключей массива. Этот код приведёт к ошибке:

1$list = [
2Status::DRAFT => 'draft',
3// …
4];

В RFC от Никиты Попова предлагается изменение такого поведения, но он ещё не перешёл в стадию голосования.

Пока что вы можете использовать перечисления в качестве ключей только в SplObjectStorage и WeakMaps.

Трейты

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

Reflection и атрибуты

Как и ожидалось, добавлено несколько Reflection-классов для работы с перечислениями: ReflectionEnum, ReflectionEnumUnitCase и ReflectionEnumBackedCase. Также появилась новая функция enum_exists, название которой говорит само за себя.

Как и обычные классы и свойства, перечисления и их варианты можно аннотировать с помощью атрибутов. Обратите внимание, перечисления будут включены в фильтр TARGET_CLASS.

И последнее: у перечислений также есть readonly-свойство $enum->name, которое в RFC упоминается как часть реализации и, вероятно, должно использоваться только для отладки. Однако об этом всё же стоит упомянуть.

Вот и всё, что можно сказать о перечислениях. Я с нетерпением жду возможности использовать их, как только выйдет PHP 8.1.