Они наконец-то появятся: поддержка перечислений будет добавлена в PHP 8.1! Пост посвящён более подробному рассмотрению нового функционала.
Начнём с того, как выглядят перечисления:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
}
Преимущество перечислений заключается в том, что они представляют собой набор постоянных значений, но, что наиболее важно, эти значения можно использовать следующим образом:
class BlogPost
{
public function __construct(
public Status $status,
) {}
}
В примере выше создание BlogPost
и передача в него перечисления выглядит так:
$post = new BlogPost(Status::DRAFT);
Не будем останавливаться на основах, поскольку, как вы уже заметили, в этом нет ничего сложного. Однако, есть ещё много дополнительных возможностей, давайте рассмотрим перечисления подробнее!
Методы перечислений
Перечисления могут определять методы, как и обычные классы. Это очень удобно, особенно в сочетании с оператором 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',
};
}
}
Перечисления и интерфейсы
Также как и классы, перечисления могут реализовывать интерфейсы:
interface HasColor
{
public function color(): string;
}
enum Status implements HasColor
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function color(): string { /* … */ }
}
Значения перечислений
Хотя перечисления являются объектами, вы можете присвоить им значения, если пожелаете; это может быть полезно, например, для сохранения их в базу данных.
enum Status: string
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}
Обратите внимание на объявление типа в определении перечисления. Он указывает на то, что все значения перечисления относятся к указанному типу. Вы также можете сделать его int
. В качестве типа можно использовать либо int
, либо string
.
enum Status: int
{
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
}
Если вы решите присвоить значения перечислениям, это будет необходимо сделать для всех вариантов, также нельзя смешивать и совмещать типы.
Типизированные перечисления с интерфейсами
Если вы используете типизированные перечисления совместно с интерфейсами, тип должен стоять сразу после имени перечисления, перед ключевым словом implements
.
enum Status: string implements HasColor
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
// …
}
Сериализация типизированных перечислений
Если вы присваиваете значения вариантам перечислений, вам, вероятно, понадобится способ их сериализации и десериализации. Под сериализацией подразумевается, что вам нужен способ получить значение перечисления. Это делается с помощью общедоступного readonly-свойства:
$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
при работе c перечислениями. Кроме того, вы можете использовать json_encode
в сочетании с типизированными перечислениями, результатом выполнения функции будет значение перечисления. Поведение можно переопределить, реализовав 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
.
Трейты
Перечисления могут использовать трейты так же, как классы, но с некоторыми ограничениями: нельзя переопределять встроенные методы перечислений, а также трейты не могут содержать свойства класса — в перечислениях свойства запрещены.
Reflection и атрибуты
Как и ожидалось, добавлено несколько Reflection-классов для работы с перечислениями: ReflectionEnum
, ReflectionEnumUnitCase
и ReflectionEnumBackedCase
. Также появилась новая функция enum_exists
, название которой говорит само за себя.
Как и обычные классы и свойства, перечисления и их варианты можно аннотировать с помощью атрибутов. Обратите внимание, перечисления будут включены в фильтр TARGET_CLASS
.
И последнее: у перечислений также есть readonly-свойство $enum->name
, которое в RFC упоминается как часть реализации и, вероятно, должно использоваться только для отладки. Однако об этом всё же стоило сказать.
Вот и всё, что можно сказать о перечислениях. Я с нетерпением жду возможности использовать их, как только выйдет PHP 8.1.
Комментарии (47)
dest2r4
02.08.2021 11:01+4осталось дождаться дженериков
Vilaine
03.08.2021 02:29Принципиально невозможны в рамках нынешней модели языка.
rjhdby
03.08.2021 11:00+1Никита Попов с вами не согласен
https://github.com/PHPGenerics/php-generics-rfc/issues/45Vilaine
03.08.2021 18:48Ну вот, сейчас я уже не могу уже найти материал, в котором они объясняли, почему нельзя сделать дженерики. Там было именно про динамическую проверку типов.
CrazyLazy
04.08.2021 15:26https://habr.com/ru/company/skyeng/blog/543794/ -- см "Что по дженерикам в PHP?" секцию.
Vilaine
04.08.2021 15:48Большое спасибо!
rjhdby — если пропустили. В целом, я думаю, ничего не изменилось.rjhdby
04.08.2021 16:17сложно != принципиально невозможно
Vilaine
04.08.2021 16:40Да всё возможно и даже не прям уж сложно. Невозможно внедрить без потерь и компромиссов в нынешнюю модель языка в рамках нынешней идеи дженериков. А посредственное изменение в ЯП будет сейчас наталкиваться на сопротивление.
rjhdby
04.08.2021 16:53Внедрение в язык новой концепции - это и есть самое, что ни на есть, изменение модели языка. И это относится к любому языку.
Дело за малым - выбрать реализацию, которая даст лучший компромис между поломкой обратной совместимости, просадкой производительности и соответствием тому, какой функционал хотели получить.
Что касается конкретно PHP и дженериков, то одним из самых мною желанных следствием будет появление нормальных типизированных коллекций из коробки.Rukis
04.08.2021 17:11Вроде уже пришли к тому, что дженериков в языке не будет, только на уровне анализа внешними инструментами. Сложилось такое впечатление после какого то интервью с Никитой.
Vilaine
04.08.2021 18:40Внедрение в язык новой концепции — это и есть самое, что ни на есть, изменение модели языка
Вовсе нет, это же какое-нибудь (уродливое) «declare(strict_types=1)». Дженерики могут быть лишь дополнительной проверкой типа на стадии выполнения, то есть это просто расширение райнтайм проверок типов. Но из-за модели языка всё будет довольно странно, неконсистентно.
Вообще рантайм проверки типов — это очень странно. Их бы убрать, внедрить дженерики как везде, и пусть сторонние инструменты проверяют на стадии сборки.
kvasvik
02.08.2021 11:45Подобные статьи надо предварять обширным разделом "нафига оно вообще надо", потому что из текста я так и не понял, чем для реализации перечисления не подходил обычный ассоциативный массив.
Rukis
02.08.2021 12:42+6// До class BlogPost { public function __construct( public string $status, ) {} } // После class BlogPost { public function __construct( public Status $status, ) {} }
fdx
02.08.2021 13:10+6Это нужно для контроля типов. Так уж получилось, что в язык его начали добавлять и мое имхо это круто. Да, приходится менять привычки.
kvasvik
02.08.2021 19:31Контроль типов конечно бывает полезным, вот только назвать перечисление отдельным типом данных можно с большой натяжкой. По своей сути, ENUM это неизменяемый массив, поэтому и реализовывать его логично через массив, а не городить особые структуры. Кроме того, это разумно с точки зрения дальнейшей поддержки проекта: когда Ваше начальство вдруг решит, что в BlogPost должно быть не 5, а 100500 статусов, и каждый пользователь ещё может добавлять свои - меньше переделывать. Так что эту привычку я пожалуй пока менять не буду.
fdx
02.08.2021 19:55+2Суть в том что Вам достаточно объявить тип Enum чтобы исключить "левые статусы" и это будет контролировать PHP, а также сократит код на проверку существования передаваемых статусов. PostBlog возможно не самый лучший пример для Enum. Для 100500 статусов я думаю лучше найти другое решение.
DEamON_M
02.08.2021 22:04Сейчас делается так (минимальный пример, по хорошему статусы надо вынести в какой-нить трейт, если они будут использоваться не только в этом классе)
<?php class Blog { const STATUS_ACTIVE = 1; const STATUS_DISABLE = 2; static function getStatuses() { return [ self::STATUS_ACTIVE => 'Включено', self::STATUS_DISABLE => 'Выключено', ]; } }
И в случае, когда потом надо переделать на статусы, которые будут создавать юзеры или еще какие-то, то в функции
Blog::getStatuses()
просто делается запрос в базу (условно) и получаются все новосозданные статусы, так как теперь (по правкам) их создают юзеры (в базе предварительно будут созданы статусы 1 - включено, 2 - выключено). Правки тут получаются минимальны и логика всех текущих кусков кода не ломается, только данные теперь не берутся из базы и можно спокойно править дальше. Как быстро провернуть такое с Enum - вопрос.Vilaine
03.08.2021 02:35+1С таким вариантом использования перечислений невозможно сделать функцию «function setStatus($status)», чтобы она тайпчекалась.
Стоило бы ещё добавить экспорт внутренних классов, как в Java, чтобы не плодить файлы.Как быстро провернуть такое с Enum — вопрос.
Не всё нужно делать быстро. Иногда стоит делать долго. В работе программиста и так очень много транзакционных издержек.
Rukis
03.08.2021 01:04+2вот только назвать перечисление отдельным типом данных можно с большой натяжкой. По своей сути, ENUM это неизменяемый массив, поэтому и реализовывать его логично через массив, а не городить особые структуры.
По своей сути enum это набор констант связанных одним типом.
Кроме того, это разумно с точки зрения дальнейшей поддержки проекта ...
С описанной вами точки зрения лесом идет любая типизация и в целом ооп.
На практике же всегда находится компромисс и если где то по логике напрашивается enum и вероятность, что это в ближайшее время переродится во что то иное - низкая, то собственно и используется enum. И даже если когда то будет решено дать пользователям расширять, условно, список статусов, то мы, по крайней мере, вплоть до этого момента будем контролировать тип, пользоваться благами ide, автодополнением, авторефакторингом, стат анализом и тп.
yatutmaster
02.08.2021 21:02Всегда использовал BenSampo\Enum\Enum , и проблем с этим не было, уже есть куча пакетов для этого, зачем внедрять это в php не понимаю. лучше развивали язык на асинхронность, параллельность
vanxant
03.08.2021 03:14-1Кому нужен параллельный пых?
Асинхронность тоже сомнительна. Ну т.е. для нее есть задачи, но ее начнут пихать туда, где она нафиг не нужна.fdx
04.08.2021 14:05Да в тех же микросервисах, если например нужно получить данные их нескольких источников. Да и не только для них. А что касается того что начнут пихать туда куда не надо, так это проблема не только PHP.
AgentCoop
Схватился за сердце после нововедений PHP. Интересно я один такой, кому не нравится в какую сторону пошло развитие PHP ? Взять тот же enum: казалось бы простой тип, но нагородили вокруг него функционал классов. Страшно представить что будет творится и как это будет использоваться на проектах.
Вот честно, рукалицо.
Arashi5
Соглсен. Складывается ощущение, что из языка делают фреймворк, чтобы засунуть его в фрэймворк.
Mozhaiskiy
Скорее этого требует комьюнити. PHP был прекрасен в своей лаконичной простоте, но это обернулось и злом — появилось такое количество популярного говнокода, что пришлось наворачивать эти самые "фреймворки фреймворков". В итоге запросы идут не столько на причёсывание и стройность языка, сколько на глубины ООП, всякую асинхронность и разные модные записи лямбд и стрелочных функций.
Хотя справедливости ради, появление контроля типов и именованные параметры функций — это офигенно и снимает кучу костылей.
Другой вопрос, что с какого-то момента погружения в новые тренды языка встаёт вопрос: а зачем я вообще делаю всё это на PHP? Если разработчик настолько вырос, что активно использует трейты, лямбды, замыкания и асинхронность, то не проще ли уже перейти на какой-нибудь Go в новом проекте?
Лично мне не очень нравится то, что из PHP делают "суперязык", в котором будет по чуть-чуть всего модного, но размывая этим его область применения и понимание его преимуществ. ИМХО, PHP великолепно выстроен под идеологию "запустился-отработал-умер", а попытка сделать из него демона "типа swoole" напоминает попытки написать веб-сервер на 1C. Можно, но зачем?
Vilaine
Так не применяйте эту асинхронность. Все остальные нововведения принимаются для выразительности или гарантий.
Mozhaiskiy
Я к тому, что нововведения в PHP не столько решают старые проблемы, сколько создают из него супер-монстра на все случи жизни. Не говоря о том, что стремительно растёт сложность синтаксиса. И что-то мне подсказывает, что крутейшую оптимизацию и JIT 7-8 версии очень быстро убьют на уровне кода, плодя бесконечные фабрики фабрик и фреймворки фреймворков.
a-tk
Жабостайл...
PrinceKorwin
Ну почему же жабостайл сразу... Это, скорее, дань функциональному программированию.
А в Java enum's покалеченные немного.
rjhdby
Они там именно, что перечисления, чем и хороши. А для всяких странных вещей недавно `sealed class` из котлина спёрли :)
PrinceKorwin
Ну как бы да :)
a-tk
Так объекты как экземпляры перечислений в Java первыми и появились. В других языках перечисление делалось на базе примитивных типов и значением перечисления и было значение underlying-типа.
Оба решения имеют свои плюсы и минусы. В Java-стиле набор допустимых значений строго ограничен, если базируемся на значениях, то мы ограничены только underlying-типом.
cl0ne
Значит, на Python вам противопоказано смотреть - там уже давно enum не слишком отличается от классов.
Как по мне, очень даже неплохое нововведение.
Vitaly48
Да и в kotlin тоже самое
MyraJKee
Согласен. Некоторые нововведения кажутся избыточными.
fdx
Это тайный заговор, чтобы PHP-сты перешли на Go :-D
rjhdby
Что значит "простой тип"? Как int или string? Как вы себе это представляете, учитывая динамическую суть PHP?
Реализация в джава стиле для PHP самая адекватная, как по функционалу, так и по возможностям реализации.
altervision
Нет, гарантированно не один. От происходящего с PHP хочется схватиться за сердце, за голову, и за револьвер. Стоило перешагнуть порог седьмой версии и дать языку гигантский толчок, как начали один за другим появляться не самые полезные нововведения. К счастью, ничто не мешает их игнорировать и по прежнему писать простой и быстрый код. И пользоваться полезной частью новшеств языка, которые улучшают производительность.
Kenya
а какие из нововведений 7х версий вы считаете не самыми полезными?