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

Они наконец-то появятся: поддержка перечислений будет добавлена в 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.

@s_panteleev
02.08.2021 10:00 UTC
Первоисточник

Комментарии

@AgentCoop
02.08.2021 06:12 UTC
+3

Схватился за сердце после нововедений PHP. Интересно я один такой, кому не нравится в какую сторону пошло развитие PHP ? Взять тот же enum: казалось бы простой тип, но нагородили вокруг него функционал классов. Страшно представить что будет творится и как это будет использоваться на проектах.

Вот честно, рукалицо.

@Arashi5
02.08.2021 06:21 UTC
0

Соглсен. Складывается ощущение, что из языка делают фреймворк, чтобы засунуть его в фрэймворк.

02.08.2021 08:04 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
02.08.2021 23:20 UTC
0
то не проще ли уже перейти на какой-нибудь Go в новом проекте?
А Go причём? Это ещё более лаконичный ЯП. Go в крупной кодовой базе будет так себе. Примерно все остальные языки более выразительные.
Можно, но зачем?
Так не применяйте эту асинхронность. Все остальные нововведения принимаются для выразительности или гарантий.
03.08.2021 11:28 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@a-tk
02.08.2021 06:38 UTC
0

Жабостайл...

02.08.2021 06:43 UTC
0

Ну почему же жабостайл сразу... Это, скорее, дань функциональному программированию.

А в Java enum's покалеченные немного.

02.08.2021 18:38 UTC
0

Они там именно, что перечисления, чем и хороши. А для всяких странных вещей недавно `sealed class` из котлина спёрли :)

02.08.2021 18:53 UTC
0

Ну как бы да :)

03.08.2021 18:52 UTC
-1

Так объекты как экземпляры перечислений в Java первыми и появились. В других языках перечисление делалось на базе примитивных типов и значением перечисления и было значение underlying-типа.

Оба решения имеют свои плюсы и минусы. В Java-стиле набор допустимых значений строго ограничен, если базируемся на значениях, то мы ограничены только underlying-типом.

@cl0ne
02.08.2021 06:51 UTC
+4

Значит, на Python вам противопоказано смотреть - там уже давно enum не слишком отличается от классов.

Как по мне, очень даже неплохое нововведение.

02.08.2021 10:31 UTC
+1

Да и в kotlin тоже самое​

@MyraJKee
02.08.2021 09:33 UTC
0

Согласен. Некоторые нововведения кажутся избыточными.

@fdx
02.08.2021 09:45 UTC
+1

Это тайный заговор, чтобы PHP-сты перешли на Go :-D

@rjhdby
02.08.2021 18:34 UTC
+2

Что значит "простой тип"? Как int или string? Как вы себе это представляете, учитывая динамическую суть PHP?
Реализация в джава стиле для PHP самая адекватная, как по функционалу, так и по возможностям реализации.

@altervision
02.08.2021 19:05 UTC
-4

Нет, гарантированно не один. От происходящего с PHP хочется схватиться за сердце, за голову, и за револьвер. Стоило перешагнуть порог седьмой версии и дать языку гигантский толчок, как начали один за другим появляться не самые полезные нововведения. К счастью, ничто не мешает их игнорировать и по прежнему писать простой и быстрый код. И пользоваться полезной частью новшеств языка, которые улучшают производительность.

03.08.2021 10:29 UTC
0

а какие из нововведений 7х версий вы считаете не самыми полезными?

@sajtpro
02.08.2021 07:59 UTC
0

прогресс уничтожает прогресс

@dest2r4
02.08.2021 08:01 UTC
+4

осталось дождаться дженериков

@Vilaine
02.08.2021 23:29 UTC
0
Принципиально невозможны в рамках нынешней модели языка.
03.08.2021 08:00 UTC
+1

Никита Попов с вами не согласен
https://github.com/PHPGenerics/php-generics-rfc/issues/45

03.08.2021 15:48 UTC
0
Ну вот, сейчас я уже не могу уже найти материал, в котором они объясняли, почему нельзя сделать дженерики. Там было именно про динамическую проверку типов.
04.08.2021 12:26 UTC
0

https://habr.com/ru/company/skyeng/blog/543794/ -- см "Что по дженерикам в PHP?" секцию.

04.08.2021 12:48 UTC
0
Большое спасибо!
rjhdby — если пропустили. В целом, я думаю, ничего не изменилось.
04.08.2021 13:17 UTC
0

сложно != принципиально невозможно

04.08.2021 13:40 UTC
0
Да всё возможно и даже не прям уж сложно. Невозможно внедрить без потерь и компромиссов в нынешнюю модель языка в рамках нынешней идеи дженериков. А посредственное изменение в ЯП будет сейчас наталкиваться на сопротивление.
04.08.2021 13:53 UTC
0

Внедрение в язык новой концепции - это и есть самое, что ни на есть, изменение модели языка. И это относится к любому языку.
Дело за малым - выбрать реализацию, которая даст лучший компромис между поломкой обратной совместимости, просадкой производительности и соответствием тому, какой функционал хотели получить.
Что касается конкретно PHP и дженериков, то одним из самых мною желанных следствием будет появление нормальных типизированных коллекций из коробки.

04.08.2021 14:11 UTC
0

Вроде уже пришли к тому, что дженериков в языке не будет, только на уровне анализа внешними инструментами. Сложилось такое впечатление после какого то интервью с Никитой.

04.08.2021 15:40 UTC
0
Внедрение в язык новой концепции — это и есть самое, что ни на есть, изменение модели языка
Вовсе нет, это же какое-нибудь (уродливое) «declare(strict_types=1)». Дженерики могут быть лишь дополнительной проверкой типа на стадии выполнения, то есть это просто расширение райнтайм проверок типов. Но из-за модели языка всё будет довольно странно, неконсистентно.
Вообще рантайм проверки типов — это очень странно. Их бы убрать, внедрить дженерики как везде, и пусть сторонние инструменты проверяют на стадии сборки.
@kvasvik
02.08.2021 08:45 UTC
0

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

@Rukis
02.08.2021 09:42 UTC
+6
// До
class BlogPost
{
    public function __construct(
        public string $status, 
    ) {}
}

// После
class BlogPost
{
    public function __construct(
        public Status $status, 
    ) {}
}
02.08.2021 09:57 UTC
-1
// Хейтеры языка и адепты "нинужного" переусложнения
class BlogPost
{
    public function __construct($status) {}
}
02.08.2021 10:52 UTC
+5

Хейтеры "усложнений" скорее напишут так:


$post = [
'status' => 'draft',

];


И сиди потом гадай, какие еще статусы бывают.

03.08.2021 05:54 UTC
-1

А що ви маєте проти *argc, **argv ??

@
02.08.2021 09:54 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@fdx
02.08.2021 10:10 UTC
+6

Это нужно для контроля типов. Так уж получилось, что в язык его начали добавлять и мое имхо это круто. Да, приходится менять привычки.

02.08.2021 16:31 UTC
0

Контроль типов конечно бывает полезным, вот только назвать перечисление отдельным типом данных можно с большой натяжкой. По своей сути, ENUM это неизменяемый массив, поэтому и реализовывать его логично через массив, а не городить особые структуры. Кроме того, это разумно с точки зрения дальнейшей поддержки проекта: когда Ваше начальство вдруг решит, что в BlogPost должно быть не 5, а 100500 статусов, и каждый пользователь ещё может добавлять свои - меньше переделывать. Так что эту привычку я пожалуй пока менять не буду.

02.08.2021 16:55 UTC
+2

Суть в том что Вам достаточно объявить тип Enum чтобы исключить "левые статусы" и это будет контролировать PHP, а также сократит код на проверку существования передаваемых статусов. PostBlog возможно не самый лучший пример для Enum. Для 100500 статусов я думаю лучше найти другое решение.

02.08.2021 19:04 UTC
0

Сейчас делается так (минимальный пример, по хорошему статусы надо вынести в какой-нить трейт, если они будут использоваться не только в этом классе)

<?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 - вопрос.

02.08.2021 23:35 UTC
+1
С таким вариантом использования перечислений невозможно сделать функцию «function setStatus($status)», чтобы она тайпчекалась.
Стоило бы ещё добавить экспорт внутренних классов, как в Java, чтобы не плодить файлы.
Как быстро провернуть такое с Enum — вопрос.
Не всё нужно делать быстро. Иногда стоит делать долго. В работе программиста и так очень много транзакционных издержек.
02.08.2021 22:04 UTC
+2

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

По своей сути enum это набор констант связанных одним типом.

Кроме того, это разумно с точки зрения дальнейшей поддержки проекта ...

С описанной вами точки зрения лесом идет любая типизация и в целом ооп.

На практике же всегда находится компромисс и если где то по логике напрашивается enum и вероятность, что это в ближайшее время переродится во что то иное - низкая, то собственно и используется enum. И даже если когда то будет решено дать пользователям расширять, условно, список статусов, то мы, по крайней мере, вплоть до этого момента будем контролировать тип, пользоваться благами ide, автодополнением, авторефакторингом, стат анализом и тп.

@He11ion
02.08.2021 14:03 UTC
+1

Хорошее нововведение, жаль что так поздно

@yatutmaster
02.08.2021 18:02 UTC
0

Всегда использовал BenSampo\Enum\Enum , и проблем с этим не было, уже есть куча пакетов для этого, зачем внедрять это в php не понимаю. лучше развивали язык на асинхронность, параллельность

@NiceDay
02.08.2021 23:35 UTC
0

ну так файберы же добавили, вот вам и асинхронность и параллельность будет.

@vanxant
03.08.2021 00:14 UTC
-1
Кому нужен параллельный пых?
Асинхронность тоже сомнительна. Ну т.е. для нее есть задачи, но ее начнут пихать туда, где она нафиг не нужна.
04.08.2021 11:05 UTC
0

Да в тех же микросервисах, если например нужно получить данные их нескольких источников. Да и не только для них. А что касается того что начнут пихать туда куда не надо, так это проблема не только PHP.

@Compolomus
03.08.2021 09:12 UTC
0

enum Status: string implements HasColor

Опечатка?