Что такое enum

Enum в PHP — это собственный тип с закрытым набором допустимых значений. Он появился в PHP 8.1 и решает задачу, которую раньше часто имитировали строками, числами или набором const: статус заказа, роль пользователя, направление сортировки, тип платежа, состояние задачи.

Главная разница в том, что enum — не просто «удобные константы». PHP проверяет тип: если функция принимает OrderStatus, туда нельзя случайно передать строку 'paid', даже если она выглядит правильно. Это продолжение темы из Типы и strict_types: enum даёт отдельный доменный тип, а не ещё одну строку с договорённостью в голове.

<?php

declare(strict_types=1);

enum OrderStatus
{
    case Draft;
    case Paid;
    case Cancelled;
}

function canShip(OrderStatus $status): bool
{
    return $status === OrderStatus::Paid;
}

var_dump(canShip(OrderStatus::Paid)); // bool(true)
// canShip('paid'); // TypeError

Каждый case — singleton-объект этого enum-типа. Поэтому сравнивать cases обычно нужно через ===, а не через строки или числа.

flowchart TD A[Enum type] --> B[Unit / pure enum] A --> C[Backed enum] B --> D[case без scalar value] C --> E[string или int value] A --> F[UnitEnum::cases] C --> G[BackedEnum::from] C --> H[BackedEnum::tryFrom] A --> I[methods и interfaces] A --> J[autoload как class-like сущность]
flowchart TD
    A[Enum type] --> B[Unit / pure enum]
    A --> C[Backed enum]
    B --> D[case без scalar value]
    C --> E[string или int value]
    A --> F[UnitEnum::cases]
    C --> G[BackedEnum::from]
    C --> H[BackedEnum::tryFrom]
    A --> I[methods и interfaces]
    A --> J[autoload как class-like сущность]
Enum задаёт закрытый набор cases; backed enum добавляет scalar-значения и методы восстановления из них.
Quick recall
Почему enum для статуса заказа надёжнее, чем строка вроде `'paid'`, если функция принимает `OrderStatus`?

Unit enum и backed enum

В PHP есть два основных вида enum. Unit enum, его ещё называют pure enum, не имеет scalar-значения у cases. В примере выше OrderStatus::Paid — это не 'Paid', не 1 и не 'paid'; это отдельный объект-case.

Backed enum имеет backing type: string или int. Это удобно, когда значение должно жить в базе, JSON, URL, HTML-форме или внешнем API.

<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Draft = 'draft';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
}

$status = OrderStatus::Paid;

echo $status->name;  // Paid
echo $status->value; // paid

name есть у любого enum-case и соответствует имени case в коде. value есть только у backed enum. Значения backed enum должны быть явно заданы и уникальны; PHP не генерирует автоматические 0, 1, 2, как в некоторых других языках. В одном enum нельзя смешать int и string.

Практическое правило простое: если значение нужно только внутри кода, начинайте с unit enum. Если оно должно стабильно сохраняться или обмениваться с внешним миром, используйте backed enum и выбирайте человекочитаемые строки, например 'paid', а не случайные числа.

Quick recall
Что именно представляет собой `OrderStatus::Paid` в unit enum?

from, tryFrom и доверие к данным

Backed enum автоматически получает методы from() и tryFrom(). Оба превращают scalar-значение обратно в enum-case, но по-разному ведут себя при неизвестном значении.

<?php

declare(strict_types=1);

enum PaymentMethod: string
{
    case Card = 'card';
    case BankTransfer = 'bank_transfer';
    case Cash = 'cash';
}

$trusted = PaymentMethod::from('card');
$maybe = PaymentMethod::tryFrom($_GET['method'] ?? '');

if ($maybe === null) {
    // Показать 400 Bad Request, выбрать дефолт или вернуть ошибку формы.
}

from() подходит для данных, которые приложение считает корректными: например, значение уже пришло из собственного кода или из колонки БД с контролируемым набором значений. Если case не найден, будет ValueError.

tryFrom() лучше для ввода пользователя, query string, webhook payload и старых данных, где неизвестное значение — ожидаемый сценарий. Он возвращает null, и вызывающий код сам решает, что делать. Важно помнить связь со strict_types: from() и tryFrom() подчиняются обычным правилам строгой и слабой типизации для scalar-параметров.

Quick recall
Что произойдёт при `PaymentMethod::from('crypto')`, если backed enum не содержит case со значением `'crypto'`?

Методы, интерфейсы и match

Enum может содержать методы и реализовывать интерфейсы. Это сильнее, чем держать рядом массив status => label: поведение живёт рядом с допустимыми значениями, а статический анализ лучше понимает связь.

<?php

declare(strict_types=1);

interface HasLabel
{
    public function label(): string;
}

enum OrderStatus: string implements HasLabel
{
    case Draft = 'draft';
    case Paid = 'paid';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Draft => 'Черновик',
            self::Paid => 'Оплачен',
            self::Cancelled => 'Отменён',
        };
    }

    public function canBeEdited(): bool
    {
        return $this === self::Draft;
    }
}

Внутри метода доступен $this, то есть конкретный case. Для различий между cases часто используют match ($this): он строгий и хорошо показывает, что все состояния перечислены явно. Это особенно полезно для PHPDoc, generics и статический анализ: анализатору проще заметить, что новый case добавили, а match забыли обновить.

У enum есть ограничения. Нельзя объявлять обычные свойства, конструктор и деструктор; enum нельзя наследовать через extends, и его cases нельзя создать через new. Если вам нужен объект с произвольным состоянием, используйте обычный class из Классы, объекты и видимость. Enum хорош именно для конечного набора вариантов.

cases() и списки для UI

Любой enum реализует внутренний интерфейс UnitEnum, поэтому у него есть статический метод cases(). Он возвращает массив всех cases в порядке объявления.

<?php

declare(strict_types=1);

$options = [];

foreach (OrderStatus::cases() as $status) {
    $options[$status->value] = $status->label();
}

print_r($options);

Так удобно строить options для формы, документацию API, whitelist значений для фильтра или список разрешённых статусов. Но не превращайте enum в dumping ground для всего подряд. Если метод начинает ходить в базу, читать конфиг или зависеть от текущего пользователя, это уже не справочник значений, а сервисная логика.

Enum, неймспейсы и autoloading

Enum — class-like сущность. Он живёт в тех же namespace, что классы, интерфейсы и traits, и автозагружается так же, как обычный класс. Это связывает тему с Неймспейсы и use и Autoloading и PSR-4.

<?php

declare(strict_types=1);

namespace App\Order;

enum OrderStatus: string
{
    case Draft = 'draft';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
}

При PSR-4 такой enum обычно лежит в src/Order/OrderStatus.php, а в другом файле импортируется через use App\Order\OrderStatus;. Для Composer нет принципиальной разницы между class Product, interface Repository и enum OrderStatus: это объявления с fully qualified name.

Serialization, JSON и база данных

Для обычной PHP-сериализации enum имеет специальное представление: сериализуется не копия объекта со свойствами, а ссылка на конкретный enum-case. После unserialize() вы снова получаете тот же singleton-case.

С JSON важнее различать два вида enum. Backed enum кодируется своим scalar-значением, например OrderStatus::Paid становится 'paid'. Unit enum не имеет стандартного scalar-представления для JSON; если вам нужно отдавать его наружу, задайте явное правило: backed enum, отдельный mapper или JsonSerializable.

С базой данных та же логика. В таблицу обычно сохраняют $status->value, а при чтении восстанавливают enum через OrderStatus::from($row['status']) или tryFrom(). В связке с PDO и подключение к базе это даёт понятную границу: PDO возвращает scalar-данные, доменная модель работает с enum-типом.

Enum против набора constants

До PHP 8.1 часто писали так:

final class OrderStatus
{
    public const DRAFT = 'draft';
    public const PAID = 'paid';
    public const CANCELLED = 'cancelled';
}

Это лучше, чем разбросанные строки, но всё ещё слабее enum. Константы не создают отдельный тип: функция с параметром string $status примет любую строку. Нельзя штатно получить список допустимых значений без Reflection или ручного массива. Поведение вроде label() приходится держать отдельно.

Enum закрывает эти дыры: тип проверяется PHP, список cases доступен через cases(), поведение можно положить рядом, а backed value остаётся удобным для хранения. Constants всё ещё уместны для технических значений без конечного набора состояний: лимиты, имена заголовков, числовые настройки. Но для доменных вариантов enum обычно честнее.

См. также

Sources

  1. PHP Manual: Basic enumerations
  2. PHP Manual: Backed enumerations
  3. PHP Manual: UnitEnum interface
  4. PHP Manual: BackedEnum interface
  5. PHP Manual: Enumeration methods
  6. PHP Manual: Enumeration static methods
  7. PHP Manual: Differences from objects
  8. PHP Manual: Value listing
  9. PHP Manual: Serialization