Перейти к содержимому
Теория · Наследование и интерфейсы · урок 7 из 8

Перечисления enum

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

В программах тоже встречаются такие закрытые наборы: масть карты, статус заказа, роль пользователя. Если хранить их строками, опечатка 'lvie' вместо 'live' пройдёт молча, а функция с параметром string примет любую строку. Перечисление решает обе проблемы: оно задаёт полный список допустимых значений, и других у него не бывает.

Чистое перечисление

Перечисление — тип с закрытым списком значений, объявленный словом enum (PHP 8.1). Каждое значение в нём называется вариантом и объявляется словом case:

PHP
<?php

enum Suit
{
    case Hearts;
    case Spades;
}

$card = Suit::Hearts;
echo $card->name, "\n"; // Hearts
var_dump($card === Suit::Hearts); // bool(true)
echo count(Suit::cases()), "\n"; // 2

Давайте разберём по шагам:

  1. enum Suit объявляет перечисление с двумя вариантами: Hearts и Spades.
  2. Suit::Hearts — вариант. Это объект, и во всей программе он существует в единственном экземпляре.
  3. ->name даёт имя варианта строкой, ровно так, как оно написано в коде.
  4. === сравнивает варианты: $card и Suit::Hearts — один и тот же объект, поэтому ответ true.
  5. Suit::cases() возвращает массив всех вариантов в порядке объявления.

Здесь картинка с монетами неточна. Настоящих рублёвых монет миллионы, а вариант Suit::Hearts один на всю программу. Создать ещё один через new нельзя: new Suit() даёт Error: Cannot instantiate enum Suit.

Перечисление в match и в типе

Перечисление удобно разбирать через match, а в объявлении функции оно работает как тип параметра:

PHP
function color(Suit $s): string
{
    return match ($s) {
        Suit::Hearts => 'red',
        Suit::Spades => 'black',
    };
}

echo color(Suit::Spades), "\n"; // black

Параметр с типом Suit принимает только варианты этого перечисления. Вызов color('Hearts') со строкой даёт TypeError, и опечатка не пройдёт незамеченной. В перечислении можно объявлять и методы, как в классе: тогда color() переезжает внутрь Suit и разбирает match ($this).

Типизированное перечисление

У варианта чистого перечисления есть только имя. Но статус заказа приходится хранить в базе данных, отправлять в JSON и получать из запроса, а там нужен скаляр — строка или число. Для этого есть типизированное перечисление: после имени ставят двоеточие и тип string или int, а каждому варианту дают значение:

PHP
enum Status: string
{
    case Draft = 'draft';
    case Live = 'live';
}

$s = Status::from('live');
echo $s->name, ' ', $s->value, "\n"; // Live live
var_dump(Status::tryFrom('archived')); // NULL
  1. case Live = 'live'; связывает вариант Live со строкой 'live'.
  2. ->name по-прежнему даёт имя варианта, Live, а ->value — его значение, live.
  3. Status::from('live') находит вариант по значению. Так строка из базы превращается обратно в вариант.
  4. Status::tryFrom('archived') такого варианта не находит и возвращает null.

Разницу между from и tryFrom часто спрашивают на собеседовании. from на неизвестном значении бросает ValueError, а tryFrom спокойно возвращает null. Поэтому tryFrom берут для данных снаружи, где неизвестное значение — обычное дело, а from — там, где неизвестное значение означает поломку.

Частая ошибка

Строку из запроса передали в from:

PHP
$input = 'archived';
$status = Status::from($input); // ✗
TEXT
Fatal error: Uncaught ValueError: "archived" is not a valid backing value for enum Status

Пользователь может прислать любую строку, и на первой же незнакомой скрипт остановится. Для ввода берите tryFrom и задавайте запасной вариант через ??: Status::tryFrom($input) ?? Status::Draft.

Главное

  • enum задаёт закрытый список вариантов; каждый вариант — единственный объект, и сравнивают их через ===.
  • ->name даёт имя варианта, а cases() — массив всех вариантов.
  • У типизированного перечисления (enum Status: string) у каждого варианта есть ещё ->value.
  • from() бросает ValueError на неизвестном значении, а tryFrom() возвращает null.