Перечисления enum
Представьте набор монет. Монетный двор чеканит рубль, два, пять и десять, и монеты в три рубля не бывает: в кошельке окажется только монета одного из выпущенных номиналов. Кассиру не нужно проверять, бывает ли такой номинал: других в обращении нет.
В программах тоже встречаются такие закрытые наборы: масть карты, статус заказа, роль пользователя. Если хранить их строками, опечатка 'lvie' вместо 'live' пройдёт молча, а функция с параметром string примет любую строку. Перечисление решает обе проблемы: оно задаёт полный список допустимых значений, и других у него не бывает.
Чистое перечисление
Перечисление — тип с закрытым списком значений, объявленный словом enum (PHP 8.1). Каждое значение в нём называется вариантом и объявляется словом case:
<?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Давайте разберём по шагам:
enum Suitобъявляет перечисление с двумя вариантами:HeartsиSpades.Suit::Hearts— вариант. Это объект, и во всей программе он существует в единственном экземпляре.->nameдаёт имя варианта строкой, ровно так, как оно написано в коде.===сравнивает варианты:$cardиSuit::Hearts— один и тот же объект, поэтому ответtrue.Suit::cases()возвращает массив всех вариантов в порядке объявления.
Здесь картинка с монетами неточна. Настоящих рублёвых монет миллионы, а вариант Suit::Hearts один на всю программу. Создать ещё один через new нельзя: new Suit() даёт Error: Cannot instantiate enum Suit.
Перечисление в match и в типе
Перечисление удобно разбирать через match, а в объявлении функции оно работает как тип параметра:
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, а каждому варианту дают значение:
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')); // NULLcase Live = 'live';связывает вариантLiveсо строкой'live'.->nameпо-прежнему даёт имя варианта,Live, а->value— его значение,live.Status::from('live')находит вариант по значению. Так строка из базы превращается обратно в вариант.Status::tryFrom('archived')такого варианта не находит и возвращаетnull.
Разницу между from и tryFrom часто спрашивают на собеседовании. from на неизвестном значении бросает ValueError, а tryFrom спокойно возвращает null. Поэтому tryFrom берут для данных снаружи, где неизвестное значение — обычное дело, а from — там, где неизвестное значение означает поломку.
Частая ошибка
Строку из запроса передали в from:
$input = 'archived';
$status = Status::from($input); // ✗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.