php

PHP Enum 8.1: перечисления, методы, интерфейсы и хранение в БД

До PHP 8.1 «перечисления» в проектах изображали как угодно: набором констант класса, строковыми литералами вроде 'active' или магическими числами. Всё это легко сломать опечаткой и невозможно типизировать. Нативный enum закрывает проблему: это полноценный тип, который ограничивает значение фиксированным набором вариантов, ловит ошибки на этапе компиляции и работает с проверкой типов в аргументах и свойствах. Разберём enum на рабочих примерах — от базового объявления до методов, интерфейсов и хранения в базе данных.

Pure enum против Backed enum

В PHP есть два вида перечислений. Pure enum (чистый) — это просто набор именованных вариантов без скалярного значения. Каждый case внутри представляет собой объект-синглтон: два обращения к одному варианту — это буквально один и тот же объект, поэтому сравнивать их можно через ===.

<?php
// Чистый enum — без привязки к значению
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

$card = Suit::Hearts;

// Сравнение по идентичности — это синглтоны
var_dump($card === Suit::Hearts); // bool(true)
var_dump($card instanceof Suit);  // bool(true)

// У каждого case есть имя
echo $card->name; // Hearts

Backed enum (типизированный) привязывает каждый вариант к скалярному значению — string или int. Это нужно, когда значение надо сериализовать: записать в базу, отдать в JSON или принять из формы. Тип объявляется после двоеточия, а значение читается через свойство value.

<?php
// Backed enum, привязанный к строке
enum Status: string
{
    case Active  = 'active';
    case Banned  = 'banned';
    case Pending = 'pending';
}

$status = Status::Active;

echo $status->name;  // Active
echo $status->value; // active

// Тип значения должен совпадать у всех case:
// нельзя смешивать string и int в одном enum
Сравнение pure enum и backed enum в PHP 8.1
Два вида enum: чистый (без значения) и типизированный (привязан к string или int)

from, tryFrom и cases: получаем значения обратно

Главная сила backed enum — преобразование скалярного значения обратно в объект перечисления. Для этого есть два статических метода. from() бросает \ValueError, если значение не найдено, а tryFrom() в этом случае возвращает null. На практике tryFrom() почти всегда удобнее: он позволяет обработать невалидный ввод без исключений.

<?php
// Значение из базы или запроса
$raw = $_GET['status'] ?? '';

// Строгий вариант: упадёт с ValueError при мусоре
$status = Status::from('active'); // Status::Active

// Безопасный вариант: вернёт null
$status = Status::tryFrom($raw) ?? Status::Pending;

// from при неизвестном значении:
try {
    Status::from('deleted');
} catch (\ValueError $e) {
    echo $e->getMessage();
    // "deleted" is not a valid backing value for enum Status
}

Метод cases() доступен у обоих видов enum и возвращает массив всех вариантов в порядке объявления. Это удобно для построения выпадающих списков, валидации и миграций.

<?php
// Собираем массив для <select> в форме
$options = [];
foreach (Status::cases() as $case) {
    $options[$case->value] = $case->name;
}
// ['active' => 'Active', 'banned' => 'Banned', 'pending' => 'Pending']

// Список допустимых значений для валидации
$allowed = array_column(Status::cases(), 'value');
// ['active', 'banned', 'pending']

Методы и константы внутри enum

Enum в PHP — это не просто список, а почти полноценный класс: в нём можно объявлять методы, константы и даже статические методы. Внутри метода доступен $this, указывающий на текущий вариант. Это позволяет держать логику, связанную с перечислением, рядом с ним — а не размазывать её по бесконечным switch в коде.

<?php
enum Status: string
{
    case Active  = 'active';
    case Banned  = 'banned';
    case Pending = 'pending';

    // Метод экземпляра: $this — текущий case
    public function label(): string
    {
        return match($this) {
            Status::Active  => 'Активен',
            Status::Banned  => 'Заблокирован',
            Status::Pending => 'Ожидает проверки',
        };
    }

    // Можно вернуть и другой тип, например цвет для UI
    public function color(): string
    {
        return match($this) {
            Status::Active  => '#22c55e',
            Status::Banned  => '#ef4444',
            Status::Pending => '#f59e0b',
        };
    }

    // Статический метод-фабрика
    public static function default(): self
    {
        return self::Pending;
    }

    // Константы внутри enum тоже разрешены
    const DEFAULT_LABEL = 'Неизвестно';
}

echo Status::Active->label();   // Активен
echo Status::Banned->color();   // #ef4444
echo Status::default()->value;  // pending

Связка match($this) с методами enum — типичный паттерн. В отличие от switch, match строгий (сравнивает через ===) и выбрасывает \UnhandledMatchError, если вы добавили новый case, но забыли описать его в методе. Это превращает забытую ветку из тихого бага в явную ошибку.


Enum с интерфейсами и в типах

Перечисления могут реализовывать интерфейсы — это позволяет принимать разные enum в одной функции по общему контракту. Backed enum при этом автоматически реализует встроенный интерфейс BackedEnum, а любой enum — UnitEnum.

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

enum Priority: int implements HasLabel
{
    case Low    = 1;
    case Medium = 2;
    case High   = 3;

    public function label(): string
    {
        return match($this) {
            Priority::Low    => 'Низкий',
            Priority::Medium => 'Средний',
            Priority::High   => 'Высокий',
        };
    }
}

// Функция принимает enum как полноценный тип
function renderBadge(HasLabel $item): string
{
    return '<span class="badge">' . htmlspecialchars($item->label()) . '</span>';
}

echo renderBadge(Priority::High); // <span class="badge">Высокий</span>

// Enum как тип аргумента — невалидное значение просто не передать
function setPriority(Priority $p): void
{
    // здесь $p гарантированно один из трёх вариантов
}

Главное преимущество — типобезопасность сигнатур. Если функция объявляет Priority $p, передать туда случайное число или строку уже не получится: PHP отклонит вызов с TypeError. Никаких проверок «а вдруг пришло не то значение» внутри метода писать не нужно.


Хранение enum в базе данных

В реальном коде enum чаще всего ездит между приложением и базой. Схема простая: при записи отдаём ->value, при чтении восстанавливаем через tryFrom(). Покажу на чистом PDO.

<?php
// Запись: в базу уходит строка, а не объект
$stmt = $pdo->prepare(
    'UPDATE users SET status = :status WHERE id = :id'
);
$stmt->execute([
    'status' => Status::Active->value, // 'active'
    'id'     => $userId,
]);

// Чтение: строку из БД превращаем обратно в enum
$row = $pdo->query('SELECT status FROM users WHERE id = 1')
            ->fetch(PDO::FETCH_ASSOC);

$status = Status::tryFrom($row['status']);

if ($status === null) {
    // в базе оказалось неизвестное значение — данные испорчены
    $status = Status::default();
}

echo $status->label(); // Активен

Если вы используете Doctrine или Eloquent, ручное преобразование не нужно — оба ORM умеют кастовать колонку в enum автоматически. В Laravel это делается через свойство $casts:

<?php
// app/Models/User.php (Laravel 9+)
class User extends Model
{
    protected $casts = [
        'status' => Status::class, // строка из БД -> enum и обратно
    ];
}

// Теперь $user->status — это объект Status, а не строка
$user->status = Status::Banned; // в базу уйдёт 'banned'
echo $user->status->label();    // Заблокирован

Чеклист по работе с enum

  • Pure enum — когда значение нужно только внутри кода (состояния, режимы). Сравнивайте через ===.
  • Backed enum — когда значение уходит в базу, JSON или форму. Тип string читабельнее в БД, int компактнее.
  • Для разбора внешнего ввода используйте tryFrom(), а не from() — это избавит от лишних try/catch.
  • Логику варианта (label, цвет, права) держите в методах enum через match($this), а не в разбросанных switch.
  • match внутри метода защищает от забытых вариантов: новый case без ветки сразу даст ошибку.
  • Используйте enum как тип аргумента — это убирает ручную валидацию «допустимого значения».
  • Реализуйте интерфейсы, если нужно обрабатывать несколько перечислений единообразно.
  • В ORM (Laravel $casts, Doctrine enumType) преобразование автоматическое — не дублируйте его руками.

Нативный enum убирает целый класс ошибок, связанных с «магическими» строками и числами, и переносит проверку корректности с рантайма на уровень типов. Если проект уже на PHP 8.1+, заменять старые наборы констант на enum стоит при первой же возможности — код становится строже и понятнее без единой дополнительной строчки валидации.