Enums в PHP: руководство по безопасному кодированию

Как разработчик, вы наверняка сталкивались с ситуациями, когда переменная могла принимать только одно из небольшого набора возможных значений. Например, переменная, содержащая статус пользователя, может иметь только возможности «Активен», «Неактивен» или «Приостановлен».

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

Разве не было бы здорово иметь возможность объявлять этот ограниченный набор возможных значений в ясной, самодокументируемой форме?

Именно здесь на помощь и приходят Перечисления, или Enums, как их часто называют.

Перечисления уже много лет являются частью многих языков программирования.

Они позволяют вам определить тип, ограниченный определенным набором значений, что повышает ясность и безопасность.

Для PHP-разработчиков хорошей новостью является то, что начиная с PHP 8.1 Enums теперь являются частью ядра PHP. Да, вы не ослышались! PHP теперь предоставляет встроенную поддержку Enums.

Краткая история Enums в PHP

До PHP 8.1 в PHP не было встроенной поддержки Enums. Хотя отсутствие Enums не мешало разработчикам писать код, оно означало, что в PHP не было инструмента, который можно найти во многих других языках, который мог бы сделать кодирование более безопасным и эффективным.

Без встроенных Enums разработчикам приходилось использовать другие конструкции для представления набора возможных значений, часто прибегая к константам классов, массивам, а иногда просто к строкам или целым числам.

Однако это могло привести к потенциальным ошибкам и часто приводило к тому, что код был не настолько понятен, насколько мог бы быть.

Появление Enums в PHP 8.1 стало существенным дополнением к языку.

Реализация Enums в PHP более мощная, чем во многих других языках, поскольку Enums в PHP — это не просто целочисленные или строковые значения.

В PHP Enum — это особый тип объекта, причем Enum является объектом одного экземпляра этого класса. Это означает, что вы можете использовать Enum везде, где только можно использовать объект, что делает их чрезвычайно универсальными.

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

Понимание основ Enums в PHP

Чтобы разобраться в основах Enums в PHP, давайте окунемся в волшебный мир Гарри Поттера.

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

В Хогвартсе всего четыре факультета: Гриффиндор, Хаффлпафф, Рейвенкло и Слизерин.

Итак, мы могли бы представить это как Enum в PHP.

Вот как вы объявляете Enum:

<?php
 
enum House
{
    case Gryffindor;
    case Hufflepuff;
    case Ravenclaw;
    case Slytherin;
}     

В этом примере мы создали Enum с именем House, который может иметь четыре возможных значения — Gryffindor, Hufflepuff, Ravenclawи Slytherin.

Давайте создадим Student класс, в котором каждый ученик владеет $house собственностью.

class Student
{
    public ?House $house = null;
}

У нас также есть SortingHat класс с sort методом.

Этот метод либо принимает предложение дома, либо случайным образом назначает дом студенту.

Как и в серии о Гарри Поттере, Распределяющая шляпа приняла во внимание выбор Гарри!

class SortingHat
{
    public function sort(Student $student, ?House $suggestion = null)
    {
        if ($suggestion) {
            $student->house = $suggestion;
 
            return;
        }
 
        $houses = [
            House::Gryffindor,
            House::Hufflepuff,
            House::Ravenclaw,
            House::Slytherin,
        ];
 
        $index = array_rand($houses);
 
        $student->house = $houses[$index];
    }
}

Как видите, мы ограничили значения свойств $suggestion и $house только одним из четырех факультетов Хогвартса, использующих Enum House.

Если вы попытаетесь отсортировать студента по несуществующему дому, PHP Enum уловит это и не запустится. В этом магия Enums!

В этом сценарии факультеты Хогвартса без связанных данных называются «Чистыми случаями», а Enums, содержащее только чистые случаи, например, наше перечисление факультетов, называется «Чистым Enum».

Чистые Enums или поддерживаемые Enums

Поддерживаемые Enums — это перечисления, поддерживаемые скалярным значением. Именно поэтому их считают «нечистыми».

Напоминаем, что скалярное значение в PHP имеет тип bool, или .floatintstring

Иногда код говорит больше, чем слова, поэтому вот Backed Enum:

<?php
 
enum House: string
{
    case Gryffindor = 'Gryffindor';
    case Hufflepuff = 'Hufflepuff';
    case Ravenclaw = 'Ravenclaw';
    case Slytherin = 'Slytherin';
} 

Как видите, мы определили тип, который поддерживает Enum (string в данном случае), и присваиваем значение каждому случаю. Не может быть проще. Но зачем вам использовать поддерживаемые Enums?

Вот отличный вариант использования:

// Let's pretend we fetched this value from the database.
$house = 'Gryffindor';
 
$student = new Student(
    House::from($house)
);
 
// object(Student)#1 (1) {
//   ["house"]=>
//   enum(House::Gryffindor)
// }
var_dump($student);

В этом примере:

  1. Мы притворяемся, что получили данные из базы данных, и пытаемся создать новый Student объект.
  2. Мы инициализируем $house свойство статическим House::from() методом из строкового значения (поскольку House теперь это поддерживаемое Enum типа string).
  3. Если это не удается, генерируется исключение. ( Uncaught ValueError: "Foo" is not a valid backing value for enum "House")

В некоторых случаях вместо того, чтобы создавать исключение, вы можете захотеть вернуться к значению по умолчанию. Вот как это можно сделать с помощью House::tryFrom() статического метода:

$student = new Student(
    House::tryFrom('Slytherin') ?? House::Gryffindor
);

Список Enums значений

В предыдущем примере вы видели, что без предложения ученика дом назначается случайным образом.

Однако в приведенном нами примере нас беспокоит то, что мы вручную перечислили возможные значения Enum House для построения нашего массива.

К счастью, у всех Enums есть cases() метод, который может сделать наш код более гибким!

class SortingHat
{
    public function sort(Student $student, ?House $suggestion = null)
    {
        if ($suggestion) {
            $student->house = $suggestion;
 
            return;
        }
 
        // Без cases()
        $houses = [
            House::Gryffindor,
            House::Hufflepuff,
            House::Ravenclaw,
            House::Slytherin,
        ];
        
        // С cases()
        $houses = House::cases(); 
 
        $index = array_rand($houses);
 
        $student->house = $houses[$index];
    }
}

Довольно аккуратно, правда?

Глубокое погружение в Enums PHP и их сравнение с классами

В нашем путешествии по Enums PHP вы могли заметить, что они похожи на классы.

Они могут иметь пространство имен, реализовывать интерфейсы, использовать черты, а также автоматически загружаться таким же образом.

Но, конечно, между классами PHP и Enums есть некоторые существенные различия.

Давайте вернемся к нашему волшебному примеру с Гарри Поттером, чтобы понять эти различия.

Когда Сортировочная шляпа распределяет учащегося по домам, мы знаем, что дом всегда будет одним из четырех предопределенных вариантов — Gryffindor, Hufflepuff, Ravenclawили Slytherin.

Других возможностей в контексте Хогвартса нет. Этот сценарий идеально подходит для Enums.

Enum, как и House, представляет собой уникальный тип данных, который включает набор предопределенных констант.

Это означает, что переменная может иметь только одну из этих предопределенных констант и ничего больше. Например, $house свойство класса Student может содержать только один из четырех вариантов дома, определенных в House Enum.

class Student
{
    public ?House $house = null;
}

Напротив, классы в PHP, такие как наш Student класс, могут содержать множество различных свойств и могут создаваться несколько раз с разными значениями свойств.

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

Еще одно отличие заключается в том, как мы сравниваем классы и Enums.

В PHP Enums сравниваются по их идентификаторам, а не по их значениям.

Давайте посмотрим на пример:

$a = House::Gryffindor;
$b = House::Gryffindor;
 
var_dump($a === $b); // bool(true)

В этом примере $a и $b являются одним и тем же House экземпляром Enum, как $a === $b и true.

Сравнения с использованием операторов «меньше» < или «больше» > не имеют смысла для объектов Enum и всегда возвращают значение false.

В PHP кейсы Enum имеют специальное свойство name, которое является чувствительным к регистру именем самого кейса. Это может быть полезно, если вы хотите напечатать имя случая Enum.

echo House::Gryffindor->name; // Prints "Gryffindor".      

Работа с методами Enums

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

Как и классы, Enums в PHP могут содержать методы. Давайте посмотрим, как мы можем использовать эту функцию, используя наш сценарий сортировки Гарри Поттера.

enum House
{
    case Gryffindor;
    case Hufflepuff;
    case Ravenclaw;
    case Slytherin;
 
    public function getHouseColors() : array
    {
        return match($this) {
            House::Gryffindor => ['Red', 'Gold'],
            House::Hufflepuff => ['Yellow', 'Black'],
            House::Ravenclaw => ['Blue', 'Bronze'],
            House::Slytherin => ['Green', 'Silver'],
        };
    }
}
 
// array(2) {
//   [0]=>
//   string(3) "Red"
//   [1]=>
//   string(4) "Gold"
// }
var_dump(House::Gryffindor->getHouseColors());

В волшебном мире Хогвартса каждый дом имеет свои цвета. В нашем примере выше мы добавили getHouseColor() в House Enum метод, возвращающий цвет каждого дома.

Когда метод определен внутри Enum, $this переменная определяется и ссылается на экземпляр случая.

Enums могут использовать трейты и реализовывать интерфейсы.

Как и классы, Enums могут использовать Traits. Это здорово, когда методов много, и вам нужно разделить их на несколько файлов, чтобы код был более аккуратным.

Однако есть некоторые ограничения:

  • У вас не может быть переменных.
  • Вы не можете переопределить методы Enums (например, values()).
trait Colors
{
    public function getHouseColors() : array
    {
        return match($this) {
            House::Gryffindor => ['Red', 'Gold'],
            House::Hufflepuff => ['Yellow', 'Black'],
            House::Ravenclaw => ['Blue', 'Bronze'],
            House::Slytherin => ['Green', 'Silver'],
        };
    }
}
 
enum House
{
    use Colors;
 
    case Gryffindor;
    case Hufflepuff;
    case Ravenclaw;
    case Slytherin;
}

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

interface HasColors
{
    public function getHouseColors() : array;
}
 
enum House implements HasColors
{
    case Gryffindor;
    case Hufflepuff;
    case Ravenclaw;
    case Slytherin;
 
    public function getHouseColors() : array
    {
        return match($this) {
            House::Gryffindor => ['Red', 'Gold'],
            House::Hufflepuff => ['Yellow', 'Black'],
            House::Ravenclaw => ['Blue', 'Bronze'],
            House::Slytherin => ['Green', 'Silver'],
        };
    }
}