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


  1. AgentCoop
    02.08.2021 09:12
    +3

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

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


    1. Arashi5
      02.08.2021 09:21

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


      1. Mozhaiskiy
        02.08.2021 11:04

        Скорее этого требует комьюнити. PHP был прекрасен в своей лаконичной простоте, но это обернулось и злом — появилось такое количество популярного говнокода, что пришлось наворачивать эти самые "фреймворки фреймворков". В итоге запросы идут не столько на причёсывание и стройность языка, сколько на глубины ООП, всякую асинхронность и разные модные записи лямбд и стрелочных функций.

        Хотя справедливости ради, появление контроля типов и именованные параметры функций — это офигенно и снимает кучу костылей.

        Другой вопрос, что с какого-то момента погружения в новые тренды языка встаёт вопрос: а зачем я вообще делаю всё это на PHP? Если разработчик настолько вырос, что активно использует трейты, лямбды, замыкания и асинхронность, то не проще ли уже перейти на какой-нибудь Go в новом проекте?

        Лично мне не очень нравится то, что из PHP делают "суперязык", в котором будет по чуть-чуть всего модного, но размывая этим его область применения и понимание его преимуществ. ИМХО, PHP великолепно выстроен под идеологию "запустился-отработал-умер", а попытка сделать из него демона "типа swoole" напоминает попытки написать веб-сервер на 1C. Можно, но зачем?


        1. Vilaine
          03.08.2021 02:20

          то не проще ли уже перейти на какой-нибудь Go в новом проекте?
          А Go причём? Это ещё более лаконичный ЯП. Go в крупной кодовой базе будет так себе. Примерно все остальные языки более выразительные.
          Можно, но зачем?
          Так не применяйте эту асинхронность. Все остальные нововведения принимаются для выразительности или гарантий.


          1. Mozhaiskiy
            03.08.2021 14:28
            -3

            Я к тому, что нововведения в PHP не столько решают старые проблемы, сколько создают из него супер-монстра на все случи жизни. Не говоря о том, что стремительно растёт сложность синтаксиса. И что-то мне подсказывает, что крутейшую оптимизацию и JIT 7-8 версии очень быстро убьют на уровне кода, плодя бесконечные фабрики фабрик и фреймворки фреймворков.


    1. a-tk
      02.08.2021 09:38

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


      1. PrinceKorwin
        02.08.2021 09:43

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

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


        1. rjhdby
          02.08.2021 21:38

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


          1. PrinceKorwin
            02.08.2021 21:53

            Ну как бы да :)


        1. a-tk
          03.08.2021 21:52
          -1

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

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


    1. cl0ne
      02.08.2021 09:51
      +4

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

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


      1. Vitaly48
        02.08.2021 13:31
        +1

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


    1. MyraJKee
      02.08.2021 12:33

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


    1. fdx
      02.08.2021 12:45
      +1

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


    1. rjhdby
      02.08.2021 21:34
      +2

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


    1. altervision
      02.08.2021 22:05
      -4

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


      1. Kenya
        03.08.2021 13:29

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


  1. sajtpro
    02.08.2021 10:59

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


  1. dest2r4
    02.08.2021 11:01
    +4

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


    1. Vilaine
      03.08.2021 02:29

      Принципиально невозможны в рамках нынешней модели языка.


      1. rjhdby
        03.08.2021 11:00
        +1

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


        1. Vilaine
          03.08.2021 18:48

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


          1. CrazyLazy
            04.08.2021 15:26

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


            1. Vilaine
              04.08.2021 15:48

              Большое спасибо!
              rjhdby — если пропустили. В целом, я думаю, ничего не изменилось.


              1. rjhdby
                04.08.2021 16:17

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


                1. Vilaine
                  04.08.2021 16:40

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


                  1. rjhdby
                    04.08.2021 16:53

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


                    1. Rukis
                      04.08.2021 17:11

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


                    1. Vilaine
                      04.08.2021 18:40

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


  1. kvasvik
    02.08.2021 11:45

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


    1. Rukis
      02.08.2021 12:42
      +6

      // До
      class BlogPost
      {
          public function __construct(
              public string $status, 
          ) {}
      }
      
      // После
      class BlogPost
      {
          public function __construct(
              public Status $status, 
          ) {}
      }


      1. Bonio
        02.08.2021 12:57
        -1

        // Хейтеры языка и адепты "нинужного" переусложнения
        class BlogPost
        {
            public function __construct($status) {}
        }
        


        1. Sabubu
          02.08.2021 13:52
          +5

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


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

          ];


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


          1. Medok-lviv-ua
            03.08.2021 08:54
            -1

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


    1. oxidmod
      02.08.2021 12:54
      +1

      Контроль типов?


    1. fdx
      02.08.2021 13:10
      +6

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


      1. kvasvik
        02.08.2021 19:31

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


        1. fdx
          02.08.2021 19:55
          +2

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


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


            1. Vilaine
              03.08.2021 02:35
              +1

              С таким вариантом использования перечислений невозможно сделать функцию «function setStatus($status)», чтобы она тайпчекалась.
              Стоило бы ещё добавить экспорт внутренних классов, как в Java, чтобы не плодить файлы.

              Как быстро провернуть такое с Enum — вопрос.
              Не всё нужно делать быстро. Иногда стоит делать долго. В работе программиста и так очень много транзакционных издержек.


        1. Rukis
          03.08.2021 01:04
          +2

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

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

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

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

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


  1. He11ion
    02.08.2021 17:03
    +1

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


  1. yatutmaster
    02.08.2021 21:02

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


    1. NiceDay
      03.08.2021 02:35

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


    1. vanxant
      03.08.2021 03:14
      -1

      Кому нужен параллельный пых?
      Асинхронность тоже сомнительна. Ну т.е. для нее есть задачи, но ее начнут пихать туда, где она нафиг не нужна.


      1. fdx
        04.08.2021 14:05

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


  1. Compolomus
    03.08.2021 12:12

    enum Status: string implements HasColor

    Опечатка?