PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 51 · От основ к архитектуре
Enum и readonly: допустимые варианты и неизменяемые значения
Моделируем статусы заказа, валидируем внешние строки и отделяем запрет переприсваивания от глубокой неизменяемости.
Выбрать один допустимый статус заказа
В одном месте написали paid, в другом — payed, и оплаченный заказ исчез из фильтра. Enum задаёт закрытый список вариантов: программа выбирает известный case, а не произвольную строку.
Что получится. Получим статус Оплачен из OrderStatus::Paid. Затем разберём внешний код и неизменяемую цену отдельно.
- OrderStatus объявляет разрешённые случаи
- Заказ хранит конкретный case Paid
- match выбирает подпись для этого case
- Внешние строки переводятся в enum на границе
<?php
declare(strict_types=1);
enum OrderStatus
{
case New;
case Paid;
}
$status = OrderStatus::Paid;
echo match ($status) {
OrderStatus::New => 'Новый',
OrderStatus::Paid => 'Оплачен',
}, "\n";
Ожидаемый результат
Оплачен
enum объявляет перечисление; case — один именованный вариант. :: выбирает его у типа. $status хранит объект этого варианта; new для enum не применяют. match сравнивает выбранный вариант и возвращает подпись. Enum появился в PHP 8.1.
Значение для БД и подпись для человека различаются
Enum ограничивает варианты, readonly — запись поля
Статус заказа и дата дедлайна решают разные задачи: нельзя путать допустимое значение с глубокой неизменяемостью.
- Enum: проверяем строку статуса
OrderStatus объявляет New, Paid и Cancelled.
- tryFrom('paid') → Paid
Строка превращается в один известный вариант. 'refunded' возвращает null.
- canMoveTo проверяет переход
Из New в Paid разрешено по правилам примера. Enum сам не подтверждает платёж.
- Readonly: поле указывает на дату
После допустимой инициализации нельзя просто переприсвоить свойство at.
- Внутри DateTime
modify меняет тот же вложенный объект: 12 октября становится 13 октября.
- Внутри DateTimeImmutable
modify возвращает другую дату; исходная внутри дедлайна остаётся 12 октября.
Readonly не замораживает весь граф вложенных объектов. Особые правила клонирования и инициализации менялись по версиям PHP; сравнение показывает обычное использование объектов из примера.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case New = 'new';
case Paid = 'paid';
public function label(): string
{
return match ($this) { self::New => 'Новый', self::Paid => 'Оплачен' };
}
}
$status = OrderStatus::Paid;
echo 'Для БД: ', $status->value, "\n";
echo 'Для страницы: ', $status->label(), "\n";
Ожидаемый результат
Для БД: paid
Для страницы: Оплачен
: string задаёт скалярное значение для каждого case; это backed enum. value — его сохраняемое значение, name — имя Paid в коде. Все значения одного backed enum уникальны и одного объявленного типа: string либо int. Перевод подписи не требует менять сохранённый paid.
Enum ограничивает варианты, а метод — переход между ними
<?php
declare(strict_types=1);
enum OrderStatus: string { case New = 'new'; case Paid = 'paid'; }
class Order
{
private OrderStatus $status = OrderStatus::New;
public function markPaid(): string
{
if ($this->status === OrderStatus::Paid) { return 'Заказ уже оплачен'; }
$this->status = OrderStatus::Paid;
return 'Заказ отмечен оплаченным';
}
}
$order = new Order();
echo $order->markPaid(), "\n";
echo $order->markPaid(), "\n";
Ожидаемый результат
Заказ отмечен оплаченным
Заказ уже оплачен
Enum не знает, когда допустимо менять состояние: правило находится в markPaid. Этот метод только показывает модель перехода; реальный заказ отмечают после проверенного подтверждения платежа. Идемпотентность платёжного запроса и транзакция БД требуют отдельной реализации.
Внешняя строка может не быть известным статусом
<?php
declare(strict_types=1);
enum OrderStatus: string { case New = 'new'; case Paid = 'paid'; }
foreach (['paid', 'payed'] as $input) {
$status = OrderStatus::tryFrom($input);
echo $status === null ? 'Неизвестный статус: ' . $input : 'Фильтр: ' . $status->value;
echo "\n";
}
try {
OrderStatus::from('payed');
} catch (ValueError $error) {
echo "В сохранённых данных неизвестный код\n";
}
Ожидаемый результат
Фильтр: paid
Неизвестный статус: payed
В сохранённых данных неизвестный код
tryFrom возвращает case или null для неизвестного значения подходящего типа. from возвращает case или бросает ValueError. Неподходящий тип входа может дать TypeError; tryFrom не обещает принять массив из формы. Перед вызовом проверяйте структуру внешнего ввода.
Readonly фиксирует поле, enum фиксирует набор вариантов
<?php
declare(strict_types=1);
final class PriceSnapshot
{
public function __construct(public readonly int $kopecks) {}
}
$price = new PriceSnapshot(25900);
echo 'В заказе: ', $price->kopecks, " коп.\n";
try {
$price->kopecks = 23900;
} catch (Error $error) {
echo "Историческая цена защищена от перезаписи\n";
}
Ожидаемый результат
В заказе: 25900 коп.
Историческая цена защищена от перезаписи
readonly-свойство допускает инициализацию по правилам своей области, но запрещает обычную последующую запись. Тип обязателен. Оно не проверяет положительность цены: добавьте проверку конструктора, если цена приходит извне. Цена в заказе и сегодняшняя цена каталога могут различаться.
<?php
declare(strict_types=1);
class Address { public string $city = 'Тула'; }
class Snapshot
{
public function __construct(public readonly Address $address) {}
}
$snapshot = new Snapshot(new Address());
$snapshot->address->city = 'Казань';
echo 'Адрес после изменения: ', $snapshot->address->city, "\n";
Ожидаемый результат
Адрес после изменения: Казань
Поле address по-прежнему содержит тот же объект, поэтому присваивания этому readonly-полю не было. Изменился вложенный Address. Для настоящего неизменяемого снимка нужен неизменяемый адрес либо отдельные скалярные поля.
Версии: явно различать базовое правило и расширения
| Версия | Возможность |
|---|---|
| PHP 8.1 | Enum и readonly-свойства |
| PHP 8.2 | readonly class распространяет ограничения на свойства класса |
| PHP 8.3 | Повторная инициализация readonly внутри __clone по специальному правилу |
| PHP 8.4 | Область установки readonly по умолчанию protected(set), если не ограничена явно |
| PHP 8.5 | clone с массивом замен, в том числе readonly-полей копии |
readonly class не допускает обычных изменяемых и динамических свойств; свойства должны иметь тип, static-свойства не подходят. Дочерний readonly-класс и его родитель должны соблюдать правила readonly-иерархии. Это не рекурсивное замораживание всех вложенных объектов.
Сохранённый код — договор с внешними данными
Изменить русскую подпись Paid можно без миграции. Переименовать case или изменить value, который уже записан в БД, — другое дело. Читайте старые данные явно, переводите код по миграционному правилу и только затем удаляйте устаревший вариант. backed enum удобно кодируется в JSON своим скалярным value; pure enum без отдельного представления не даёт такого договора автоматически.
Enum может иметь методы, константы, реализовывать интерфейсы и использовать допустимые трейты, но не хранит обычные свойства экземпляра или static-свойства. Cases — фиксированные объекты; их не создают, не клонируют и не наследуют как обычный класс.
Добавить отмену, не открывая произвольную запись статуса
Попробуйте. Добавьте Cancelled = "cancelled". Разрешите отменять только новый заказ.
Разобрать решение
Обновите enum и все исчерпывающие match. В Order добавьте cancel(), который проверяет текущий New до присваивания Cancelled. Отмена оплаченного заказа должна идти отдельным процессом возврата, а не просто переписыванием поля.
Если набор статусов редактирует администратор в БД без выпуска нового кода, фиксированный enum может не подходить. Используйте проверенные записи справочника и описанный договор переходов.
Дальше: полный договор языка и специальные случаи
Далее разобраны методы enum, сериализация, числовой вход, допустимые места case-констант и стабильность сохранённого формата. Эти правила поддерживают реальный переход между страницей, БД и API.
Разделы ниже сохраняют специальные правила официального PHP Manual: Enum с поведением: подпись, интерфейс, фабрика и значение по умолчанию, Enum на границе JSON и PHP: имя, значение и тот же case, Числовой код статуса: допустимое значение и допустимый тип, Один выбранный case в настройках, свойствах и значениях по умолчанию, Вариант перечисления как готовое правило расчёта, Стабильный JSON и устаревшая сохранённая строка enum.
Enum с поведением: подпись, интерфейс, фабрика и значение по умолчанию
Форма доставки показывает два способа получения. У них есть название, правило выбора и единый список допустимых вариантов. Enum может хранить эти правила рядом с case. Сначала нужны самые простые case, интерфейс и переиспользуемый метод.
В первом опыте перечисление pure: после enum нет : string или : int, а у case нет =. Его case — полноценный объект с name, но без value. Implements обещает метод подписи; трейт только повторно использует реализацию. Новое состояние внутрь case мы не записываем.
- Объявлены Pickup и Courier
- suggestedFor(20) выбирает Courier
- label возвращает Курьер
- DEFAULT ссылается на Pickup и не добавляет третий case
<?php
declare(strict_types=1);
interface HasDeliveryLabel
{
public function label(): string;
}
trait DeliveryDescription
{
public function describe(): string
{
return 'Способ: ' . $this->label();
}
}
enum DeliveryMethod implements HasDeliveryLabel
{
use DeliveryDescription;
case Pickup;
case Courier;
public const DEFAULT = self::Pickup;
public function label(): string
{
return match ($this) {
self::Pickup => 'Самовывоз',
self::Courier => 'Курьер',
};
}
public static function suggestedFor(int $weight): self
{
if ($weight < 0) {
throw new InvalidArgumentException('Вес не может быть отрицательным');
}
return $weight > 10 ? self::Courier : self::Pickup;
}
}
function printDelivery(HasDeliveryLabel $delivery): void
{
echo $delivery->label(), "\n";
}
function defaultDelivery(DeliveryMethod $delivery = DeliveryMethod::DEFAULT): string
{
return $delivery->name;
}
printDelivery(DeliveryMethod::suggestedFor(20));
echo DeliveryMethod::Pickup->describe(), "\n";
echo defaultDelivery(), "\n";
echo count(DeliveryMethod::cases()), "\n";
Ожидаемый результат
Курьер
Способ: Самовывоз
Pickup
2
SuggestedFor — наша политика учебной доставки, не встроенное правило enum. Статический метод возвращает существующий case. DEFAULT — ссылка на существующий Pickup, поэтому cases по-прежнему содержит два варианта, в порядке объявления. Case разрешён как значение константы и параметра по умолчанию.
| Разрешено | Зачем использовать |
|---|---|
| Методы экземпляра | Поведение конкретного варианта: подпись или доступный переход. |
| Статические методы | Выбрать существующий case по проверенным входным данным. |
| Интерфейсы и трейты без свойств | Повторить поведение и выполнить общий договор. |
| Константы | Дать имя правилу или удобный alias уже существующему case. |
В enum нельзя объявлять свойства, конструктор и деструктор или наследоваться от другого enum/класса. Трейт со свойством тоже не подойдёт. Это сохраняет case как неизменяемый именованный вариант; данные конкретной доставки — адрес, вес, номер заказа — храните в отдельном объекте рядом с выбранным DeliveryMethod.
Enum уже реализует UnitEnum; backed enum также реализует BackedEnum. Объявлять эти встроенные интерфейсы вручную в обычном классе не следует. Из магических методов перечислению разрешены __call, __callStatic и __invoke, но не произвольная смена состояния через __get/__set.
Закрытое множество case позволяет перечислить все варианты в match. Добавили новый case — нужно обновить места, где он обрабатывается; отсутствие default помогает обнаружить забытый вариант как UnhandledMatchError. Это удобно для стабильных состояний кода. Каталог способов доставки, который администратор редактирует в БД каждый день, чаще должен быть таблицей данных, а не enum.
Enum на границе JSON и PHP: имя, значение и тот же case
В PHP статус — case, в API нужен скалярный код. В from и tryFrom разобрано движение со входа в enum. Теперь посмотрим обратное преобразование и отличие JSON от внутренней сериализации.
- В PHP выбран ShipmentCode::Ready
- JSON получает скаляр ready
- Внутренняя сериализация запоминает конкретный case
- Восстановление возвращает тот же case, а не его клон
<?php
declare(strict_types=1);
enum ShipmentCode: string
{
case Ready = 'ready';
case Sent = 'sent';
}
enum PickupPoint
{
case North;
}
$status = ShipmentCode::Ready;
echo json_encode($status, JSON_THROW_ON_ERROR), "\n";
$saved = serialize($status);
$restored = unserialize($saved);
echo $restored === $status ? "Тот же case\n" : "Другой case\n";
try {
json_encode(PickupPoint::North, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
echo "Для pure enum нужен явный формат JSON\n";
}
echo json_encode(['point' => PickupPoint::North->name], JSON_THROW_ON_ERROR), "\n";
try {
$copy = clone $status;
} catch (Error $error) {
echo "Case нельзя клонировать\n";
}
Ожидаемый результат
"ready"
Тот же case
Для pure enum нужен явный формат JSON
{"point":"North"}
Case нельзя клонировать
Backed enum по умолчанию кодируется как своё value. Pure enum без JsonSerializable с JSON_THROW_ON_ERROR вызывает JsonException. В отдельной явной форме мы выбираем name сами. Серийная строка создана здесь же и доверена: этот опыт не разрешает принимать unserialize-ввод из запроса.
Enum нельзя создать через new: используйте объявленные case, cases, а для backed enum — from/tryFrom. Восстановление существующего case через serialize/unserialize возвращает ту же идентичность, а не ещё один изменяемый экземпляр. Переименование класса или case может сломать старые сохранённые строки.
Опция allowed_classes у unserialize не применяется к enum, поэтому её нельзя считать проверкой допустимого перечисления. В доверенном внутреннем формате всё равно явно проверьте ожидаемый тип результата; внешнее API лучше принимать как скалярный код с валидацией и from/tryFrom, а не как сериализованный объект.
JsonSerializable позволяет enum явно определить иной JSON-формат. Делайте это только по договору API: изменение скаляра на объект затронет клиентов. В чистом enum выбор name также становится внешним договором, поэтому безопаснее отдельно назначить стабильный код, если название case хочется свободно рефакторить.
Для int-backed enum передавайте проверенное int. From/tryFrom подчиняются режиму типов: tryFrom не превращает любое неверное по типу значение в null, TypeError остаётся возможным. Null означает отсутствие case с допустимым значением, а не универсальную успешную обработку повреждённого входа.
Числовой код статуса: допустимое значение и допустимый тип
Старая система хранит состояние счёта числами: 10 — выставлен, 20 — оплачен. Внутри программы удобнее писать InvoiceState::Paid. Число остаётся форматом обмена, а case получает имя. Это продолжение первого перечисления; ошибки типов разобраны в объявлениях типов функции.
- В хранилище находится число 20
- InvoiceState::from(20) находит Paid
- В PHP работаем с именованным case
- Для сохранения читаем value: снова 20
| Запись | Что она означает |
|---|---|
| enum InvoiceState: int | Все варианты этого перечисления имеют целочисленный код. |
| case Paid = 20; | Имя Paid связано с уникальным кодом 20; PHP не назначает следующие номера сам. |
| InvoiceState::from(20) | Найти вариант; неизвестный допустимый по типу код вызовет ValueError. |
| InvoiceState::tryFrom(99) | При допустимом типе, но неизвестном коде вернуть null. |
| $state->name / $state->value | Имя варианта в коде / числовой код обмена; это разные данные. |
<?php
declare(strict_types=1);
enum InvoiceState: int
{
case Issued = 10;
case Paid = 20;
}
$state = InvoiceState::from(20);
echo $state->name, ': ', $state->value, "\n";
echo InvoiceState::tryFrom(99) === null ? "Неизвестный код\n" : "Код найден\n";
try {
InvoiceState::from(99);
} catch (ValueError $error) {
echo "Нельзя восстановить статус 99\n";
}
try {
$state->value = 30;
} catch (Error $error) {
echo "Код case изменить нельзя\n";
}
Ожидаемый результат
Paid: 20
Неизвестный код
Нельзя восстановить статус 99
Код case изменить нельзя
Каждая ветка показывает свою ситуацию. Null здесь не означает «счёт не оплачен»: он означает, что мы вообще не узнали статус. Value доступно только для чтения; нельзя и связать с ним изменяемую переменную через &.
Теперь источник передал строку «20». Решение зависит от файла, который вызывает from/tryFrom. Declare(strict_types=1) в другом подключённом файле не переключает все вызовы процесса. В следующем ZIP два файла: example.php со строгим вызовом и weak-reader.php с явно указанным strict_types=0. Так различие видно, а не спрятано в настройке сервера.
<?php
declare(strict_types=1);
enum InvoiceState: int
{
case Issued = 10;
case Paid = 20;
}
require __DIR__ . '/weak-reader.php';
echo 'Нестрого: ', weakState('20')->name, "\n";
echo 'Нестрого, float: ', weakState(20.0)->name, "\n";
foreach (['20', 20.0, [20]] as $input) {
try {
InvoiceState::tryFrom($input);
} catch (TypeError $error) {
echo 'Строго: тип ', get_debug_type($input), " отклонён\n";
}
}
function stateFromForm(mixed $raw): ?InvoiceState
{
if (!is_string($raw) || !in_array($raw, ['10', '20'], true)) {
return null;
}
return InvoiceState::from((int) $raw);
}
echo 'Проверенная форма: ', stateFromForm('20')?->name ?? 'ошибка', "\n";
echo stateFromForm('20abc') === null ? "Повреждённая строка отклонена\n" : "Принято\n";
Ожидаемый результат
Нестрого: Paid
Нестрого, float: Paid
Строго: тип string отклонён
Строго: тип float отклонён
Строго: тип array отклонён
Проверенная форма: Paid
Повреждённая строка отклонена
WeakState получает mixed и вызывает from именно из нестрогого файла. Число 20.0 выбрано без потери дробной части. Строгий tryFrom не заменяет проверку типа. Наш stateFromForm принимает только две разрешённые строки, затем намеренно переводит их в int. Это узкий договор формы с двумя вариантами, не универсальный разбор всех чисел.
Не начинайте проверку с (int)$raw: строка «20abc» при таком преобразовании потеряет хвост. Для большого внешнего набора чисел сначала нужна отдельная проверка формата и диапазона, затем tryFrom. Нестрогое приведение дробных float может потерять точность и выдать deprecation; денежные значения и коды статусов не следует передавать через float. У одного backed enum все case должны иметь один вид кода — int либо string, без смеси и повторяющихся значений.
Попробуйте сами. Что нужно изменить, если внешний сервис добавил код 30 «возврат»?
Разбор решения
Согласовать смысл нового состояния, добавить case Refunded = 30, обновить разрешённые значения формы и места обработки. Просто принимать любое число недостаточно: неизвестный код по-прежнему нельзя считать известным состоянием.
Один выбранный case в настройках, свойствах и значениях по умолчанию
По умолчанию отчёт скачивают как CSV. Вместо четырёх строк «csv» в разных местах можно выбрать один case. Константное выражение описывает заранее допустимое значение объявления; оно не запускает произвольный метод ради вычисления настройки. Перед опытом нужны const и константы и локальная static-переменная.
- ReportFormat::Csv — существующий вариант
- DEFAULT_REPORT хранит тот же case
- Свойство и параметр получают этот вариант по умолчанию
- Name/value дают подпись и внешний код без создания нового case
Двоеточие string задаёт внешний текстовый код. Const хранит выбранный вариант, public объявляет свойство объекта, а static внутри функции сохраняет локальную переменную между вызовами. С PHP 8.2 name/value case можно читать в константных выражениях. Это разрешённое чтение свойств перечисления, а не общее разрешение вызывать методы в const.
<?php
declare(strict_types=1);
const CSV_CODE = 'csv';
enum ReportFormat: string
{
case Csv = CSV_CODE;
case Json = 'json';
}
const DEFAULT_REPORT = ReportFormat::Csv;
const DEFAULT_NAME = ReportFormat::Csv->name;
const DEFAULT_CODE = ReportFormat::Csv->value;
class ExportRequest
{
public const DEFAULT_FORMAT = ReportFormat::Csv;
public ReportFormat $format = ReportFormat::Csv;
}
function chooseFormat(ReportFormat $requested = ReportFormat::Csv): string
{
static $last = ReportFormat::Csv;
$previous = $last;
$last = $requested;
return $previous->value . ' -> ' . $requested->value;
}
echo DEFAULT_NAME, ': ', DEFAULT_CODE, "\n";
$request = new ExportRequest();
echo $request->format === DEFAULT_REPORT ? "Один вариант по умолчанию\n" : "Разные\n";
echo ExportRequest::DEFAULT_FORMAT->value, "\n";
echo chooseFormat(ReportFormat::Json), "\n";
echo chooseFormat(), "\n";
Ожидаемый результат
Csv: csv
Один вариант по умолчанию
csv
csv -> json
json -> csv
Case в значении const, свойства, параметра и static не клонируется. Локальная static хранит выбор только в текущем процессе и не сохраняет предпочтение пользователя между HTTP-запросами. CSV_CODE в backing value использует возможность PHP 8.2 ссылаться на константу.
Нельзя присвоить другому case сам объект ReportFormat::Csv как его backing value: ему нужен int или string. Нельзя подменить константное выражение вызовом метода, __invoke или чтением через ArrayAccess: это выполнение кода с возможными побочными эффектами. В обычной исполняемой строке такие вызовы допустимы, если enum реализует соответствующий договор. Ограничения относятся к месту вычисления; само слово «константа» не делает произвольную операцию безопасной.
Попробуйте сами. Замените значение по умолчанию на Json и объясните, какие объявления потребуется согласовать.
Разбор решения
В этом демонстрационном файле case указан в глобальной константе, константе класса, свойстве, параметре и static-переменной функции. В приложении используйте одну именованную константу DEFAULT_REPORT в подходящих местах, чтобы изменение политики не требовало независимых правок. Name/value примера сейчас явно относятся к Csv — их тоже следует связать с выбранным default.
Вариант перечисления как готовое правило расчёта
У небольшой службы доставки ровно два тарифа: обычный и срочный. Для каждого нужен один расчёт надбавки. Case можно передать туда, где ожидается вызываемое правило. Начните с __invoke у обычного объекта: круглые скобки после объекта вызывают этот метод.
- Выбран DeliveryTariff::Express
- calculateOrder получает case как callable
- Скобки вызывают __invoke с 1200 копейками
- Результат 1500: товары плюс фиксированная надбавка 300
Метод __invoke не хранит новый заказ внутри case: он получает сумму и возвращает результат. Static-метод standard выбирает существующий вариант. В интерфейсе объявлена эта статическая операция; реализация обязана сохранить договор. Self в объявлении enum означает этот тип перечисления.
<?php
declare(strict_types=1);
interface HasStandardTariff
{
public static function standard(): self;
}
enum DeliveryTariff implements HasStandardTariff
{
case Standard;
case Express;
public static function standard(): self
{
return self::Standard;
}
public function __invoke(int $goodsKopecks): int
{
if ($goodsKopecks < 0) {
throw new InvalidArgumentException('Сумма товаров не может быть отрицательной');
}
return $goodsKopecks + ($this === self::Express ? 300 : 100);
}
}
$tariff = DeliveryTariff::Express;
echo 'Прямой вызов: ', $tariff(1200), "\n";
function calculateOrder(callable $tariff, int $goodsKopecks): int
{
return $tariff($goodsKopecks);
}
echo 'Обычный: ', calculateOrder(DeliveryTariff::standard(), 1200), "\n";
try {
calculateOrder($tariff, -1);
} catch (InvalidArgumentException $error) {
echo "Отрицательная сумма отклонена\n";
}
Ожидаемый результат
Прямой вызов: 1500
Обычный: 1300
Отрицательная сумма отклонена
Это фиксированный учебный тариф в копейках. Вызов не обращается к службе доставки. У case нет изменяемого веса, адреса или счётчика заказов. Если тарифы задаёт администратор или нужны отдельные настройки перевозчиков, подойдёт объект стратегии либо таблица данных.
Попробуйте сами. Как сохранить тот же расчёт без магического вызова?
Разбор решения
Переименовать __invoke в total и явно вызвать $tariff->total(1200); у потребителя принимать DeliveryTariff вместо callable. Если подмена разными вызываемыми правилами не нужна, явное имя метода часто легче читать.
Стабильный JSON и устаревшая сохранённая строка enum
Мобильному приложению нужен код статуса, а оператору — понятная подпись. JsonSerializable позволяет перечислению сформировать этот внешний ответ. Отдельно посмотрим, почему внутренняя PHP-сериализация зависит от имени case. Предыдущий разбор границы JSON и enum показал стандартный формат; теперь меняем его осознанно.
Implements JsonSerializable обещает метод jsonSerialize. Его результат получает json_encode. Возвращаемый массив — форма ответа API; он не обязан совпадать с внутренним name. Unserialize может выдать предупреждение и вернуть false, если сохранённый case больше не существует. В опыте повреждаем только созданную нами локальную строку.
<?php
declare(strict_types=1);
enum ShipmentPhase implements JsonSerializable
{
case Ready;
case Sent;
public function jsonSerialize(): array
{
return match ($this) {
self::Ready => ['code' => 'ready', 'label' => 'Ready for pickup'],
self::Sent => ['code' => 'sent', 'label' => 'Handed over'],
};
}
}
echo json_encode(ShipmentPhase::Ready, JSON_THROW_ON_ERROR), "\n";
$saved = serialize(ShipmentPhase::Ready);
$same = unserialize($saved, ['allowed_classes' => false]);
echo $same === ShipmentPhase::Ready ? "Enum не отфильтрован allowed_classes\n" : "Заблокирован\n";
$oldSnapshot = str_replace(':Ready', ':Older', $saved);
$warned = false;
set_error_handler(function (int $level, string $message) use (&$warned): bool {
if ($level !== E_WARNING || !str_starts_with($message, 'unserialize():')) {
return false;
}
$warned = true;
return true;
});
try {
$restored = unserialize($oldSnapshot);
} finally {
restore_error_handler();
}
echo $warned && $restored === false ? "Старый case не восстановлен\n" : "Неожиданный результат\n";
Ожидаемый результат
{"code":"ready","label":"Ready for pickup"}
Enum не отфильтрован allowed_classes
Старый case не восстановлен
Ready и Older имеют одинаковую длину: мы моделируем неизвестное имя, а не ошибочную длину PHP-строки. Allowed_classes не действует на enum; это не разрешение принимать недоверенную сериализацию. Пример не читает запросы или внешние файлы. Подробный текст предупреждения не становится ответом пользователю.
Не редактируйте реальные сериализованные строки заменами подстрок: это только контролируемая проверка отказа. Для долговременного обмена обычно яснее стабильный scalar-код и явная версия схемы с проверкой входа. JsonSerializable работает и у backed enum, заменяя его стандартный скалярный JSON; изменение уже опубликованного формата требует согласования с потребителями. Print_r пригоден для локального осмотра name/value, но не является договором хранения.
Попробуйте сами. Добавьте case Cancelled и сформируйте для него JSON, не используя name как внешний код.
Разбор решения
Добавьте отдельную ветку match с code cancelled и собственной label. Если забыть ветку, полный match при сериализации нового case завершится UnhandledMatchError. Это сигнал обновить договор, а не повод молча подставить статус ready.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
BackedEnum::fromПолучить вариант по внешнему коду, считая отсутствие ошибкой.
PHP 8.1+ · Core
public static function from(int|string $value): staticПолучить вариант по внешнему коду, считая отсутствие ошибкой.
| Часть вызова | Договор |
|---|---|
| value | Значение подходящего backing-типа конкретного enum. |
| Возврат | Вариант этого же enum; static обозначает тип вызванного перечисления. |
| Ошибки | ValueError при неизвестном значении; TypeError при неподходящем типе с учётом режима преобразований. |
- Данные должны содержать известный код
- from(value) ищет совпадение
- Совпадение → case; нет → ValueError
<?php
enum Payment: string { case Waiting = 'wait'; case Paid = 'paid'; }
$row = ['state' => 'paid'];
$state = Payment::from($row['state']);
echo $state->name, "\n";
Ожидаемый результат
Paid
Так удобно восстанавливать строку из собственного хранилища, где неизвестный код означает нарушение данных или забытый шаг миграции. Наличие ключа/типа в реальной строке результата нужно проверить по вашему контракту.
<?php
enum Payment: string { case Waiting = 'wait'; case Paid = 'paid'; }
try { Payment::from('other'); }
catch (ValueError $error) { echo "Код не поддерживается\n"; }
Ожидаемый результат
Код не поддерживается
Перехват не делает данные корректными: обработчик должен отклонить операцию, запросить исправление или выполнить документированную миграцию. Для ожидаемой пользовательской ошибки tryFrom часто короче.
Сравните UnitEnum::cases, BackedEnum::tryFrom. Граница между строкой и enum.
Возвращает соответствующий case. Неизвестное значение вызывает ValueError; неподходящий тип и режим strict_types могут привести к TypeError. Вызывайте на конкретном enum, а не на BackedEnum напрямую.
<?php
declare(strict_types=1);
enum Status: string { case New='new'; case Paid='paid'; }
$row = ['status'=>'paid'];
echo Status::from($row['status'])->name, "\n";
Ожидаемый результат
Paid
Несовместимые старые данные должны выявляться при загрузке, а не тихо становиться new.
<?php
declare(strict_types=1);
enum Mode: int { case Preview=1; case Commit=2; }
try { Mode::from(3); } catch (ValueError $error) { echo "Неизвестный режим\n"; }
Ожидаемый результат
Неизвестный режим
Исключение поймано на границе разбора данных; рабочий код получает только корректный Mode.
Рядом: BackedEnum::tryFrom, UnitEnum::cases.
BackedEnum::tryFromНайти вариант по коду или получить null при отсутствии.
PHP 8.1+ · Core
public static function tryFrom(int|string $value): ?staticНайти вариант по коду или получить null при отсутствии.
| Часть вызова | Договор |
|---|---|
| value | Скалярное backing-значение; тип всё равно проверяется. |
| Возврат | Вариант либо null. Знак ? означает допустимый null. |
| Ошибки | Отсутствие case не даёт ValueError, но неверный тип способен вызвать TypeError. |
- Проверить тип входа
- tryFrom(value)
- null → объяснить ошибку; case → продолжить
<?php
enum Delivery: string { case Pickup = 'self'; case Courier = 'door'; }
$known = Delivery::tryFrom('door');
echo $known?->name, "\n";
var_dump(Delivery::tryFrom('unknown'));
Ожидаемый результат
Courier
NULL
?-> вызывает чтение только если слева не null. Для реального обязательного выбора явно обработайте null, а не прячьте ошибку пустой строкой.
<?php
enum Delivery: string { case Pickup = 'self'; }
try { Delivery::tryFrom(['self']); }
catch (TypeError $error) { echo "Массив не является кодом\n"; }
Ожидаемый результат
Массив не является кодом
Внешнее поле может оказаться массивом, например при имени формы delivery[]. is_string до вызова нужен даже у метода с названием try.
Сравните UnitEnum::cases, BackedEnum::from. Граница между строкой и enum.
Возвращает case при совпадении и null при отсутствии значения. Неверный тип аргумента не превращается автоматически в null: сначала проверьте вход JSON/формы.
<?php
declare(strict_types=1);
enum Delivery: string { case Pickup='pickup'; case Courier='courier'; }
$input = 'drone';
$delivery = is_string($input) ? Delivery::tryFrom($input) : null;
echo $delivery === null ? "Выберите способ доставки\n" : $delivery->value;
Ожидаемый результат
Выберите способ доставки
Неизвестное значение становится понятной ошибкой формы.
<?php
declare(strict_types=1);
enum Status: string { case New='new'; case Paid='paid'; }
$unknown = [];
foreach (['new','paid','hold'] as $raw) { if (Status::tryFrom($raw) === null) { $unknown[] = $raw; } }
echo implode(',', $unknown), "\n";
Ожидаемый результат
hold
Интеграция может добавить новый статус; фиксируем его вместо угадывания значения.
Рядом: BackedEnum::from, UnitEnum::cases.
UnitEnum::casesПолучить все варианты перечисления в порядке объявления.
PHP 8.1+ · Core
public static function cases(): arrayПолучить все варианты перечисления в порядке объявления.
| Часть вызова | Договор |
|---|---|
| Аргументы | Нет. Вызывается у конкретного enum, например OrderState::cases(). |
| Возврат | Обычный список с ключами 0…n−1, содержащий сами объекты case, не их имена/значения. |
| Порядок | Порядок объявления case. Обычные константы enum не добавляются в список. |
- cases() → [New, Paid]
- foreach получает один case
- name — имя; у backed enum value — внешний код
<?php
enum OrderState { case New; case Paid; }
foreach (OrderState::cases() as $index => $state) {
echo $index, ':', $state->name, "\n";
}
Ожидаемый результат
0:New
1:Paid
Это готовая основа выбора допустимого состояния. Отображаемые пользователю русские подписи лучше оформить отдельным методом/картой переводов, а не заставлять имя case быть текстом интерфейса.
<?php
enum Delivery: string {
case Pickup = 'self';
case Courier = 'door';
public const DEFAULT_METHOD = self::Pickup;
}
echo count(Delivery::cases()), "\n";
foreach (Delivery::cases() as $method) { echo $method->value, "\n"; }
Ожидаемый результат
2
self
door
ReflectionClass::getConstants может показать и DEFAULT_METHOD, поэтому для списка вариантов выбирайте cases. Изменение возвращённого массива не изменяет само объявление enum.
Сравните BackedEnum::from, BackedEnum::tryFrom. Граница между строкой и enum.
Возвращает список case-объектов конкретного enum. У backed-варианта доступны name и value; у чистого — name. Полезно для селекта и проверки полноты отображения.
<?php
declare(strict_types=1);
enum Delivery: string { case Pickup='pickup'; case Courier='courier'; }
echo implode(',', array_map(static fn(Delivery $d): string => $d->value, Delivery::cases())), "\n";
Ожидаемый результат
pickup,courier
При генерации HTML экранируйте значения и подписи по контексту.
<?php
declare(strict_types=1);
enum ReportMode { case Daily; case Weekly; case Monthly; }
foreach (ReportMode::cases() as $mode) { echo $mode->name, "\n"; }
Ожидаемый результат
Daily
Weekly
Monthly
Так можно построить проверку, что каждому варианту назначен форматтер или подпись.
Рядом: BackedEnum::from, BackedEnum::tryFrom.
Источники и обсуждения
- Перечисления
- Backed enums
- Readonly
- Readonly classes
- Изменения 8.3
- Изменения 8.4
- Изменения 8.5
- Pure enum
- Методы и интерфейсы
- Статические методы enum
- Константы enum
- Трейты в enum
- Case в константных выражениях
- Отличия enum от обычного объекта
- Сериализация перечислений
- Принятый RFC PHP 8.2: name/value в константных выражениях
- Собственный JSON-договор
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


