Enums в PHP 8.1 — практическое руководство

Быстрые ссылки
Основной синтаксис
Pure vs Backed Enums
Добавление методов в enums
Константы в enums
Когда использовать enums?
Миграция и чек-листы
Примеры и распространённые ошибки
Короткое объявление
Введение
Enums (перечисления) — типы данных, экземпляры которых могут принимать только заранее определённые значения (кейсы). В PHP 8.1 это полноценная часть языка: перечисления интегрированы в систему типов, поддерживают методы, интерфейсы и рефлексию. Коротко: enums повышают безопасность типов и читаемость кода.
Определение термина в одну строку:
- Enum — именованный набор возможных значений для переменной или свойства.
Важно: enums — не просто «константный класс», это полноценная конструкция языка с поведением, методами и интеграцией в типовую систему.
Основной синтаксис
Ниже — минимальный пример чистого enum, переведённый на корректную форму:
enum PostStatus {
case Published;
case InReview;
case Draft;
}Ключевое слово case, до появления enums применявшееся в switch, теперь объявляет варианты перечисления. Обращение к варианту происходит как к статическому члену класса:
$published = PostStatus::Published;Enums совместимы с системой типов. Пример конструктора, принимающего значение enum:
class BlogPost {
public function __construct(
public string $Headline,
public string $Content,
public PostStatus $Status = PostStatus::Draft
) {}
}
// Корректно
$post = new BlogPost(
"Example Post",
"An example",
PostStatus::Draft
);
// Ошибка типов: Argument #3 must be of type PostStatus
$post = new BlogPost(
"Broken Example",
"A broken example",
"Submitted"
);Первый пример работает: аргумент соответствует варианту enum. Во втором — передаётся строка, а тип требует PostStatus, поэтому PHP выбросит ошибку времени выполнения.
Чтобы получить все значения перечисления, используйте статический метод cases():
PostStatus::cases();
// => [PostStatus::Published, PostStatus::InReview, PostStatus::Draft]Pure vs Backed Enums
Чистые enums содержат только case-объявления. Backed enums — это перечисления, у которых каждый case связан с конкретным скалярным значением (string или int).
Пример backed enum с типом string:
enum PostStatus : string {
case Published = "S1";
case InReview = "S2";
case Draft = "S3";
}Зачем это нужно? Backed enum удобно хранить в базе данных: вместо хранения имени кейса можно сохранить короткий код (например, “S1”). Доступ к привязанному значению осуществляется через свойство value:
class BlogPostRepository {
public function save(BlogPost $Post) : void {
$this->insert(
"blog_posts",
[
"headline" => $Post->Headline,
"content" => $Post->Content,
"status" => $Post->Status->value
]
);
}
}
$post = new BlogPost("Example", "Demo", PostStatus::Published);
(new BlogPostRepository())->save($post);
// В БД сохранится status = "S1"Ограничения backed enums:
- Поддерживаются только string и int.
- Все значения должны быть уникальны в пределах enum.
- Нельзя использовать union string|int.
Для восстановления enum по значению есть два метода:
$status = PostStatus::from($record['status']); // бросает ValueError при неверном значении
$status = PostStatus::tryFrom($record['status']); // возвращает null при отсутствии соответствияДобавление методов в enums
Enums — это по сути классы, поэтому в них можно определять методы, использовать трейты и реализовывать интерфейсы. Это удобно для инкапсуляции поведения, связанного с конкретными кейсами.
Пример enum с методом:
enum PostStatus {
case Published;
case Draft;
public function isPubliclyAccessible(): bool {
return ($this instanceof self::Published);
}
}Реализация интерфейса и использование в коде:
enum PostStatus implements PublicAccessGatable {
case Published;
case Draft;
public function isPubliclyAccessible(): bool {
return ($this instanceof self::Published);
}
}
class UserAuthenticator {
public function shouldAllowAccess(PublicAccessGatable $Resource): bool {
return ($this->User->isAdmin() || $Resource->isPubliclyAccessible());
}
}
$auth = new UserAuthenticator();
// получить пост из БД
if (!$auth->shouldAllowAccess($post->Status)) {
http_response_code(403);
}Замечания по методам в enums:
- Можно использовать public, protected и private. В реальности protected/private действуют одинаково, потому что enums не наследуются.
- Нельзя определить конструктор construct() или деструктор destruct().
- Поддерживаются статические методы, трейты и интерфейсы.
Константы в enums
Enums могут содержать свои константы — как простые скаляры, так и ссылки на кейсы enum:
enum PostStatus {
case Published;
case Draft;
public const Live = self::Published;
public const PlainConstant = "foobar";
}Важно понимать разницу:
$published = PostStatus::Published; // это case, тип PostStatus
$plain = PostStatus::PlainConstant; // это строка "foobar"Только $published удовлетворяет typehint PostStatus. $plain — простой скаляр.
Когда использовать enums?
Enums подходят для ситуаций, где значение должно быть одним из заранее известных вариантов. Примеры:
- Состояние сущности (post status, order status, task state).
- Предопределённые роли/права.
- Форматы вывода (json, xml), каналы коммуникации и т.п.
Проблема традиционного подхода с константами состоит в том, что тип остается общим (int/string) и принимает любые значения. Enums решают это, делая контракт строгим и понятным.
Пример старого подхода и его недостатки:
class PostStatus {
const Published = 0;
const Draft = 1;
}
class BlogPost {
public function __construct(
public string $Headline,
public int $Status
) {}
}
$post = new BlogPost("My Headline", PostStatus::Published);
// Но ничего не мешает сделать:
$post = new BlogPost("My Headline", 9000);Док-блоки и внешние библиотеки помогают, но добавляют сложность. Enums — нативный, явный способ обозначить набор допустимых значений.
Примеры использования и шаблоны
Персистентность: использовать backed enum для сохранения компактного кода в БД и восстановления enum через from()/tryFrom().
Поведение: добавлять методы в enum для локальной логики (isPubliclyAccessible, canBeEditedBy(user) и т.д.).
Конфигурация: использовать enum как контракт для конфигураций и feature flags, если набор стабилен.
Интеграция: если внешняя система присылает коды, используйте tryFrom() для безопасного преобразования и ветвления при null.
Когда enums не подходят — примеры неудач
Часто меняющийся набор значений. Если варианты динамически добавляются пользователями, enum неудобен — лучше таблицу в БД и связную логику.
Большие пространства значений. Если нужно сотни уникальных значений с множеством атрибутов — enum быстро станет неудобным.
Полиморфная модель с множеством свойств. Если каждый вариант имеет собственные поля и поведение, возможно, лучше использовать отдельные классы и паттерн State/Strategy.
Альтернативные подходы
Класс с константами (традиционный PHP): прост, но слабая типизация.
Класс-обёртка с валидацией значений: можно получить преимущества, но придётся писать много шаблонного кода.
Библиотеки «fake enum»: дают дополнительные фичи, но ненужны с появлением нативных enums.
Ментальная модель: enums — это «встроенная безопасная множественность», в то время как класс с константами — «неявная множественность».
Модель зрелости (maturity levels)
- Уровень 0 — константы в классе (небезопасно).
- Уровень 1 — валидация значений вручную / helper-методы.
- Уровень 2 — использование внешних библиотек-эмуляторов (fake enums).
- Уровень 3 — нативные enums в PHP 8.1 (рекомендуется для статичных наборов значений).
Факт-бокс: что важно знать
- В PHP 8.1 enums добавлены в ноябре 2021.
- Есть два интерфейса: UnitEnum (чистые enum) и BackedEnum (enum с привязанными значениями).
- Backed enums поддерживают только string или int.
- Методы: cases(), from(), tryFrom(), свойство value для backed.
- enum_exists() и ReflectionEnum доступны для рефлексии.
Миграция: мини-методология и чек-лист
Мини-методология для перевода проекта на enums:
- Идентифицировать наборы значений, где допустимые варианты статичны.
- Выбрать type: pure или backed (для взаимодействия с БД предпочитайте backed).
- Создать enum с тестовым покрытием для всех мест использования.
- Обновить сигнатуры методов/свойств на тип enum.
- Запустить тесты и отловить все места, где передавались «сырая» строки/числа.
- Обновить сериализацию/персистенцию (Repository/ORM mapping).
- Проверить совместимость на окружениях и инструментах CI.
Чек-лист ролей:
- Разработчик: заменить константы на enum, добавить методы инкапсуляции, обновить места вызова.
- Архитектор: проверить, где enum не подходит и где лучше использовать таблицы/классы.
- QA: покрыть сценарии восстановления из БД, tryFrom и ошибки ValueError.
Playbook миграции — пошагово
- Найдите все классы/файлы, где используются повторяющиеся наборы скалярных констант.
- Создайте enum в отдельном файле с тем же набором вариантов.
- Сначала замените объявления свойств/параметров в небольших модулях и прогоните тесты.
- Для БД: добавьте трансформер в репозиторий, который при записи использует ->value, а при чтении — tryFrom/from.
- Обновите документацию и типы в API (OpenAPI/JSON Schema), если применимо.
- Разверните на staging и следите за ValueError в логах.
- Поэтапно заменяйте по проекту, фиксируя regressions.
Примеры тест-кейсов и критерии приёмки
Тесты, которые нужно покрыть:
- Создание сущности с корректным enum — проходит.
- Попытка создать сущность со «сырой» строкой — бросает TypeError.
- Сохранение в БД записывает корректный код для backed enum.
- Восстановление через from() и tryFrom() даёт ожидаемые случаи/null.
- Рефакторинг не нарушил публичный API (если это критично).
Критерии приёмки:
- Все тесты проходят локально и в CI.
- Нет необработанных ValueError в логах после деплоя на staging.
- Документация API обновлена с указанием новых типов.
Совместимость и заметки по миграции
- Enums требуют PHP >= 8.1. Для проектов, поддерживающих старые версии, переход потребует планирования миграции интерпретатора.
- ORM/провайдеры: многие ORM (Doctrine, Eloquent) уже имеют плагины/трансформеры для работы с enums, но может потребоваться обновление адаптеров.
- CLI/console: при передаче enum через аргументы командной строки, используйте строковые коды и затем tryFrom() для безопасного преобразования.
Безопасность и приватность
Enums сами по себе не добавляют уязвимостей, но изменяют поведение при приведении/сериализации. Обратите внимание:
- Не доверяйте внешним данным без валидации: при восстановлении через from() обработайте возможный ValueError.
- Для публичных API документируйте допустимые значения и используйте tryFrom() + явные ошибки 400 вместо необработанных исключений.
- Если enum-backed значения используются в ссылках/токенах, подумайте о контроле доступа и о том, не раскрывают ли коды лишнюю информацию.
Примеры кода из реального мира
Сохранение и восстановление с Doctrine-подходом (псевдокод):
// трансформер поля status
public function convertToDatabaseValue(PostStatus $status = null) {
return $status?->value;
}
public function convertToPHPValue($value): ?PostStatus {
return $value === null ? null : PostStatus::tryFrom($value);
}CLI-парсер и безопасное преобразование:
$code = $input->getArgument('status');
$status = PostStatus::tryFrom($code);
if ($status === null) {
throw new
untimeException("Неверный статус: $code");
}Частые ошибки при работе с enums
- Ожидание, что значения enum можно сравнивать с string/int напрямую. Для backed enum используйте ->value.
- Попытка наследовать enum (не поддерживается).
- Использование конструкций, которые сериализуют enum как объект без явного преобразования (предпочитайте value при персистенции).
Короткое объявление
PHP 8.1 добавляет native enums — удобный и строгий способ описывать наборы допустимых значений. Enums можно использовать как простые наборы (pure) или с привязанными значениями для персистенции (backed). Они поддерживают методы, интерфейсы и рефлексию. Enums упрощают поддержку и уменьшают количество ошибок, связанных с некорректными значениями. Для проектов, готовых перейти на PHP 8.1, внедрение enums стоит планировать по этапам: определить кандидатов, создать enum, адаптировать репозитории и обновить тесты.
Итоги
- Enums делают контракт значений явным и проверяемым языком на уровне типов.
- Для персистенции используйте backed enums и методы from()/tryFrom().
- Добавляйте поведение в enum через методы и интерфейсы, не распыляйте логику по всему коду.
- Планируйте миграцию по шагам, тщательно тестируя преобразования и сереализацию.
Важно: enum — мощный инструмент, но он не заменяет базы данных или полиморфные модели там, где наборы значений динамически расширяются или содержат много метаданных.
Дополнительно — чек-листы и шаблоны
Чек-лист для разработчика:
- Найти кандидатов на enum
- Выбрать pure или backed
- Создать enum-файл
- Обновить объявления типов
- Обновить репозитории/трансформеры
- Написать тесты на from()/tryFrom
- Проверить сериализацию
Шаблон migration commit message:
“chore: migrate PostStatus constants to native enum PostStatus (PHP 8.1) — update repositories, tests”
Социальные превью (рекомендации):
- OG title: Enums в PHP 8.1 — быстрое руководство
- OG description: Как и когда использовать pure и backed enums в PHP, примеры миграции и советы по персистенции.
Спасибо за внимание — если нужно, могу подготовить готовый diff-мигратор для конкретного репозитория или сгенерировать правила для CI, которые отлавливают передачу «сырых» значений вместо enum.