Современный PHP 8 — enum, readonly и атрибуты
Материал дополняет ООП в PHP и ориентирован на PHP 8.1+. Устаревшие константы вместо enum и "магические" строки статусов здесь рассматриваются только как антипаттерн.
Практическая цель этих возможностей простая: сделать код предсказуемым, уменьшить количество "тихих" ошибок и облегчить сопровождение команды.
Readonly-свойства (PHP 8.1)
Модификатор readonly запрещает присвоение свойству после инициализации. Запись возможна только один раз — обычно в конструкторе.
Код ITЗагрузка примера кода…
Разбор:
readonlyу свойств разрешает запись только один раз (обычно в конструкторе), после чего объект становится неизменяемым по этим полям.declare(strict_types=1);усиливает контроль типов: сигнатуры и значения проверяются строго.\DateTimeImmutableвыбран осознанно: тип даты тоже не меняется после создания, что сочетается с идеей immutable-объекта.private readonly float $amountскрывает внутреннее поле, а доступ наружу идет через методtotal().- Закомментированная строка с повторным присваиванием демонстрирует, что PHP выбросит ошибку модификации readonly-свойства.
Правила:
- Тип свойства обязателен (нельзя
public readonly $xбез типа). - Readonly нельзя сделать
static. - Свойство без значения по умолчанию должно быть инициализировано до конца конструктора.
Readonly-класс (PHP 8.2)
Если пометить весь класс как readonly, все его свойства становятся readonly автоматически:
readonly class Point
{
public function __construct(
public float $x,
public float $y,
) {}
}
Разбор:
readonly classавтоматически делает все свойства класса readonly без повторения модификатора у каждого поля.- Инициализация
xиyпроисходит в конструкторе, далее состояние точки неизменно. - Такой класс хорошо подходит для value object/DTO, где важна предсказуемость и отсутствие побочных эффектов.
- Невозможность "тихо" изменить координаты упрощает отладку и сравнение объектов в бизнес-логике.
Такой класс не может содержать обычные изменяемые свойства и не может быть унаследован не-readonly классом. Удобно для DTO и объектов-значений, которые не должны меняться после создания.
Перечисления enum (PHP 8.1)
Enum — отдельный тип для ограниченного набора вариантов. Заменяет "магические" строки и int-константы в классе.
Unit enum (без значения)
Код ITЗагрузка примера кода…
Разбор:
enum Statusвводит закрытый набор валидных состояний, исключая случайные строки вроде'publshed'с опечаткой.case Draft/Published/Archived— это экземпляры enum-кейсов с жесткой типовой семантикой.match ($status)обрабатывает каждый кейс явно, без fall-through, в отличие отswitch.- Возврат
boolизcanEdit(...)показывает, как enum напрямую управляет прикладной логикой доступа. - С таким типом статуса сигнатуры функций становятся самодокументируемыми: передать произвольную строку уже нельзя.
Сравнение — только через === между case одного enum. Status::Draft === 'draft' всегда ложь.
Backed enum (со значением в БД или JSON)
Код ITЗагрузка примера кода…
Разбор:
enum Priority: string— backed enum, где каждый кейс связан со строковым значением для хранения в БД/JSON.Priority::from('high')возвращает кейс или выбрасывает исключение при невалидном значении.Priority::tryFrom('unknown')безопаснее для внешнего ввода: возвращаетnullвместо исключения.$p->valueдает сериализуемое значение (high), а$p->name— имя кейса в коде (High).- Такой раздел между
nameиvalueудобно использовать для API-контрактов и внутренней доменной модели.
В таблице хранят колонку priority VARCHAR со значениями 'low', 'normal', 'high' и при чтении вызывают Priority::from($row['priority']).
Методы в enum
Enum может содержать методы, как класс:
Код ITЗагрузка примера кода…
Разбор:
- Методы внутри enum (
label(),canPublish()) инкапсулируют поведение рядом с данными, а не размазываютif-проверки по проекту. match ($this)делает логику отображаемого лейбла полной и типобезопасной для всех кейсов.self::Adminиself::EditorвcanPublish()формализуют правила прав доступа в одном месте.- Вызов
Role::tryFrom($input)на границе системы превращает "грязный" ввод в валидный тип до бизнес-логики. - Это снижает вероятность багов из-за неучтенных строковых ролей в контроллерах и сервисах.
Такой enum удобно использовать в API-слое: Role::tryFrom($input) дает контролируемую точку валидации входных данных перед бизнес-логикой.
Атрибуты (PHP 8.0)
Атрибут — метаданные у класса, метода, свойства или параметра. Синтаксис: #[ИмяАтрибута(...)]. Это не комментарии: атрибуты доступны через Reflection во время выполнения.
Код ITЗагрузка примера кода…
Разбор:
#[\Attribute(...)]объявляетRouteкак настоящий атрибут, а не обычный класс данных.TARGET_METHODограничивает область применения: атрибут можно ставить только на методы.- Конструктор
Routeхранит метаданные маршрута (path,methods) рядом с методом контроллера. #[Route('/about', methods: ['GET'])]показывает именованные аргументы атрибута, что делает аннотацию читаемой.- Такой подход переносит конфигурацию маршрутов ближе к коду обработчика и уменьшает дублирование в отдельных файлах.
Чтение атрибутов (упрощённый диспетчер):
$ref = new \ReflectionMethod(HomeController::class, 'index');
$attrs = $ref->getAttributes(Route::class);
foreach ($attrs as $attr) {
$route = $attr->newInstance();
echo $route->path; // /
}
Разбор:
ReflectionMethodполучает метаописание конкретного метода без его вызова.getAttributes(Route::class)фильтрует атрибуты именно типаRoute, игнорируя остальные.$attr->newInstance()материализует объект атрибута с параметрами, указанными в#[Route(...)].- Далее код читает
pathи может строить роутер динамически по этим данным. - Это база декларативного подхода: метаданные пишутся рядом с кодом, а инфраструктура считывает их автоматически.
Фреймворки (Symfony, Laravel 8+) используют встроенные и пользовательские атрибуты для маршрутов, валидации, DI. В учебном коде атрибуты помогают декларативно описывать правила без дублирования в конфигурационных массивах.
Встроенный атрибут для переопределения типов
class Container
{
/** @var array<string, object> */
private array $services = [];
#[\ReturnTypeWillChange]
public function offsetGet($key): mixed
{
return $this->services[$key] ?? null;
}
}
Разбор:
- Атрибут
#[\ReturnTypeWillChange]сообщает о временной совместимости сигнатуры с интерфейсами между версиями PHP. - Метод
offsetGet($key): mixedиллюстрирует доступ к сервису по ключу из внутреннего массива контейнера. - Конструкция
$this->services[$key] ?? nullбезопасно возвращаетnull, если сервиса нет. - Использование атрибута должно быть временным: при стабилизации сигнатур лучше перейти к явным типам без подавления предупреждений.
ReturnTypeWillChange — временная метка совместимости при реализации интерфейсов, меняющих сигнатуры между версиями PHP.
Как сочетать возможности
Пример неизменяемой сущности со статусом-enum:
readonly class Article
{
public function __construct(
public string $slug,
public string $title,
public Status $status,
) {}
public function withStatus(Status $new): self
{
return new self($this->slug, $this->title, $new);
}
}
Разбор:
Articleмоделируется как immutable-объект: поля задаются в конструкторе и не меняются на месте.- Поле
statusстрого типизировано enumStatus, что исключает случайные недопустимые состояния. - Метод
withStatus(Status $new)реализует функциональный стиль: создает новый экземпляр вместо мутации старого. - Такой подход упрощает аудит состояния, тестирование и откат изменений в сложных сценариях.
- Комбинация
readonly + enumделает доменную модель более строгой и устойчивой к ошибкам интеграции.
Вместо изменения полей создаётся новый объект — предсказуемое поведение без побочных эффектов.
В реальных проектах это хорошо работает вместе с исключениями: невалидный вход превращается в ValidationException, а валидный вход становится строгим enum и дальше безопасно проходит по системе.
Сравнение с прежними приёмами
| Задача | Старый подход | PHP 8+ |
|---|---|---|
| Статусы заказа | const STATUS_PAID = 2 | enum OrderStatus |
| Неизменяемый ID | private + только геттер | public readonly string $id |
| Метаданные маршрута | массив в отдельном файле | #[Route('...')] на методе |