В PHP 8.1 наконец-то добавили встроенную поддержку перечислений — enum. Под катом — перевод статьи блогера и PHP разработчика Брента с обзором новых возможностей, дополнениям и комментариями разработчиков о том, что они думают о поддержке перечислений в PHP 8.1.
Enum в PHP ждали — и новая функция оправдывает ожидания. Сергей Пешалов, инженер-программист из Eggheads Solutions, оценил нововведения так:
Если раньше фактически писался свой класс или просто использовали массив, то сейчас для этого есть enum. Смысл в том, что в БД поле enum было, а в php нет, поэтому для реализации использовали каждый во что горазд. Сейчас эту проблему решили. Реализация интересная. Фактически перечислители работают как классы, даже с методами. Очень удобно.
Раз удобно, давайте разберёмся подробнее в том, как работают перечисления.
Так выглядят перечисления:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
}
Преимущество enum в том, что это набор постоянных значений, которые можно вводить так:
class BlogPost
{
public function __construct(
public Status $status,
) {}
}
В этом примере мы создаём enum и передаём его BlogPost объекту так:
$post = new BlogPost(Status::DRAFT);
Хотя кейсы enum сами по себе являются объектами, их нельзя создавать с помощью new. Кроме того, перечисления имеют фиксированное количество возможных значений.
Методы enum
Enum могут определять методы, как и обычные классы. Это очень мощная функция, особенно в сочетании с оператором match:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function color(): string
{
return match($this)
{
Status::DRAFT => 'grey',
Status::PUBLISHED => 'green',
Status::ARCHIVED => 'red',
};
}
}
Методы можно использовать так:
$status = Status::ARCHIVED;
$status->color(); // 'red'
Также разрешены статические методы:
enum Status
{
// …
public static function make(): Status
{
// …
}
}
Self в перечислениях тоже применяют:
enum Status
{
// …
public function color(): string
{
return match($this)
{
self::DRAFT => 'grey',
self::PUBLISHED => 'green',
self::ARCHIVED => 'red',
};
}
Одно из наиболее важных различий между enum и классом заключается в том, что enums не могут иметь состояния. Объявление свойств для перечислений запрещены, также не допускаются статические свойства.
Магические константы
Перечисления полностью поддерживают все магические константы, которые PHP поддерживает для классов. Константа класса, которая ссылается на имя самого enum:
__CLASS__ —магическая константа, которая ссылается на имя enum из enum.
__FUNCTION__ —в контексте метода enum.
__METHOD__ — в контексте метода enum.
Запрещённые магические методы
Чтобы объекты enum не имели какого-либо состояния и чтобы два enum были сопоставимы, enums запрещает реализацию нескольких магических методов:
__get (): чтобы предотвратить сохранение состояния в объектах enum.
__set (): чтобы предотвратить присвоение динамических свойств и поддержания состояния.
__construct (): перечисления не поддерживают все конструкции new Foo ().
__destruct (): перечисления не должны поддерживать состояние.
__clone (): перечисления — это неклонируемые объекты.
__sleep (): перечисления не поддерживают методы жизненного цикла.
__wakeup (): перечисления не поддерживают методы жизненного цикла.
__set_state (): чтобы предотвратить принуждения состояния к объектам enum.
Enum интерфейсы
Enum могут реализовывать интерфейсы, как и обычные классы:
interface HasColor
{
public function color(): string;
}
enum Status implements HasColor
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function color(): string { /* … */ }
}
Перечисления, не поддерживаемые значением, автоматически реализуют интерфейс UnitEnum.
Перечисления не могут явно реализовать этот интерфейс, поскольку это делается внутри движка. Это делается только для помощи в определении типа данных enum. Метод UnitEnum :: cases возвращает массив всех случаев данного enum.
Значения перечислений
Хотя enum — это объект, вы можете присвоить им значения, чтобы сохранить их в базе данных.
enum Status: string
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}
Перечисления поддерживают пространства имён, автозагрузку, методы, но не свойства, реализующие интерфейсы и другое поведение PHP-классов.
Обратите внимание на объявление типа в определении перечислений. Так мы указываем, что все значения enum относятся к заданному типу. Аналогичным образом работает int. В качестве значений enum можно использовать только int и string.
Любой другой тип, включая типы bool, null, String, Int, не допускается.
enum Status: int
{
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
}
Если вы решите присвоить значения перечислениям, это необходимо сделать для каждого перечисления, также типы нельзя смешивать и совмещать.
PHP резервирует enum и предотвращает создание любых функций, классов, интерфейсов и так далее с именем enum. PHP учитывает зарезервированные ключевые слова в значениях пространства имён. Синтаксис аналогичен синтаксису трейта/класса/интерфейса. Само имя enum нечувствительно к регистру и соответствует тому, как PHP обрабатывает классы и функции без учёта регистра.
Максим Епихин, руководитель отдела поддержки и тестирования программного обеспечения АСУ, ФГАНУ ЦИТиС:
Enum — это отличная система организации списков. Вот такой класс я писал для этого раньше, когда перечислений не было. Он который имитирует часть enum. Сейчас я могу от него полностью избавиться. То есть все статичные данные, которые я храню классами и наследованием и интерфейсами, просто заменились на нормальный и лаконичный enum. Теперь можно было бы хранить все enum не классами (как костыль), а средствами языка.
С одной стороны, количество файлов не изменится, а просто заменятся классы на enum. С другой стороны, зачем хранить файлы, если enum можно объявлять в одном файле. Тут другая проблема: enum будут в одном файле, а это неудобно для изменения и поиска.
Главная фича в том, что мы избавляемся от своих костылей в пользу ядра движка с готовыми функциями, но получаем энтропию организации enum.
Enum нельзя наследовать от другого enum или его класса. Если класс пытается расширить enum, это также приведёт к ошибке, потому что все enum объявлены окончательными.
Типизированные enum с интерфейсами
Если вы комбинируете типизированные enum с интерфейсами, тип перечисления нужно ставить сразу после имени перечисления, перед ключевым словом implements.
enum Status: string implements HasColor
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
// …
}
Сериализация типизированных перечислений
Если вы присваиваете значения вариантам перечислений, вам понадобится способ их сериализации и десериализации. Сериализация означает, что вам нужен способ получить значение перечисления. Это делается с помощью общедоступного свойства value — только для чтения:
$value = Status::PUBLISHED->value; // 2
Получить значение перечисления можно с помощью метода :Enum::from:
$status = Status::from(2); // Status::PUBLISHED
Также можно использовать метод tryFrom, который вернёт null, если передано неизвестное значение. При использовании from будет выведено исключение:
$status = Status::from('unknown'); // ValueError
$status = Status::tryFrom('unknown'); // null
Обратите внимание, при работе с перечислениями можно использовать встроенную функцию serialize и unserialize. Кроме того, можно использовать json_encode в сочетании с типизированными перечислениями, результатом функции и будет значение enum. Это поведение можно изменить, реализовав JsonSerializable.
Вывод вариантов перечисления
Чтобы получить все доступные варианты перечислений, воспользуйтесь статичным методом :Enum::cases():
Status::cases();
/* [
Status::DRAFT,
Status::PUBLISHED,
Status::ARCHIVED
] */
Обратите внимание, в массиве содержатся объекты перечисления:
array_map(
fn(Status $status) => $status->color(),
Status::cases()
);
Перечисления — это объекты
Варианты перечислений — это объекты, на самом деле это одноэлементные объекты. Сравнить их можно так:
$statusA = Status::PENDING;
$statusB = Status::PENDING;
$statusC = Status::ARCHIVED;
$statusA === $statusB; // true
$statusA === $statusC; // false
$statusC instanceof Status; // true
Перечисления как ключи массива
Так как перечисления — объекты, сейчас их невозможно использовать в качестве ключей массива. Такой код приведёт к ошибке:
$list = [
Status::DRAFT => 'draft',
// …
];
RFC предлагает изменить это поведение, но за него ещё не проголосовали.
Пока вы можете использовать перечисления только в качестве ключей в SplObjectStorage и WeakMaps.
Трейты
Перечисления могут использовать трейты аналогично классам, но с некоторыми ограничениями. Вы не можете переопределять встроенные методы перечислений, а также они не могут содержать свойства класса, что запрещено в перечислениях.
PHP поддерживает автозагрузку для классов, трейтов и интерфейсов, enums также поддерживает автозагрузку. Обратите внимание, что для этого могут потребоваться обновления генераторов карт классов, которые анализируют файлы на предмет автоматически загружаемых элементов.
Reflection и атрибуты
Для работы с enum добавили несколько Reflection классов: ReflectionEnum, ReflectionEnumUnitCase и ReflectionEnumBackedCase. Также добавили новую функцию enum_exists — функция, название которой говорит само за себя. Обратите внимание, что из-за семантики классов функция class_exists также возвращает true для enum.
Как и обычные классы и свойства, enum и их случаи можно аннотировать с помощью атрибутов. Обратите внимание, enum включат в фильтр TARGET_CLASS.
И последнее: у перечислений также есть readonly свойство: $enum->name, которое в RFC упоминают как деталь реализации и, вероятно, его можно использовать только для отладки.
Епихин Максим Николаевич, руководитель отдел поддержки и тестирования программного обеспечения АСУ, ФГАНУ ЦИТиС:
Очень крутая и долгожданная вещь в PHP. Минусы и нюансы только в добавлении элементов. Это, насколько мне известно, не такая «дешёвая» операция. Несколько вариантов сравнения, которые работают как бы одинаково, но кто-то умудрится запороть всё и там. Пока протестировать их не удалось, но думаю, что именно в этом будут проблемы.
Комментарии (8)
Audiophile
09.09.2021 09:20+4Перевод хромает
Хотя кейсы enum сами по себе являются объектами, их нельзя создавать с помощью новой конструкции.
Имеется ввиду "с помощью ключевого слова new", я так понял.
Anastasia_rova Автор
09.09.2021 18:24Спасибо, что заметили.
FanatPHP
11.09.2021 17:00Если что - это не единственная ошибка.
Скажите, вы сами-то можете понять, что в некоторых местах этой статьи написано? Например
"Хотя enum — это объект, вы можете присвоить им значения, чтобы сохранить их в базе данных"
или
"Перечисления, не поддерживаемые значением, автоматически реализуют интерфейс UnitEnum." Кто, кого и чем тут должен поддерживать?
TonyKentnarEarth
12.09.2021 07:17Одно из наиболее важных различий между enum и классом заключается в том, что enums не могут иметь состояния.
Вроде как конкретное значение перечисления у объекта enum’а это и есть его состояние
Rsa97
Где-то я это уже видел.
habr.com/ru/post/570508
Anastasia_rova Автор
Опоздали( Но в статье честно не только перевод — есть дополнения, примеры и ссылки.