Перейти к основному содержимому

Современный 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 строго типизировано enum Status, что исключает случайные недопустимые состояния.
  • Метод withStatus(Status $new) реализует функциональный стиль: создает новый экземпляр вместо мутации старого.
  • Такой подход упрощает аудит состояния, тестирование и откат изменений в сложных сценариях.
  • Комбинация readonly + enum делает доменную модель более строгой и устойчивой к ошибкам интеграции.

Вместо изменения полей создаётся новый объект — предсказуемое поведение без побочных эффектов.

В реальных проектах это хорошо работает вместе с исключениями: невалидный вход превращается в ValidationException, а валидный вход становится строгим enum и дальше безопасно проходит по системе.


Сравнение с прежними приёмами

ЗадачаСтарый подходPHP 8+
Статусы заказаconst STATUS_PAID = 2enum OrderStatus
Неизменяемый IDprivate + только геттерpublic readonly string $id
Метаданные маршрутамассив в отдельном файле#[Route('...')] на методе

Что изучить дальше