Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Enum и readonly: допустимые варианты и неизменяемые значения
  2. Выбрать один допустимый статус заказа
  3. Значение для БД и подпись для человека различаются
  4. Enum ограничивает варианты, а метод — переход между ними
  5. Внешняя строка может не быть известным статусом
  6. Readonly фиксирует поле, enum фиксирует набор вариантов
  7. Версии: явно различать базовое правило и расширения
  8. Сохранённый код — договор с внешними данными
  9. Добавить отмену, не открывая произвольную запись статуса
  10. Дальше: полный договор языка и специальные случаи
  11. Enum с поведением: подпись, интерфейс, фабрика и значение по умолчанию
  12. Enum на границе JSON и PHP: имя, значение и тот же case
  13. Числовой код статуса: допустимое значение и допустимый тип
  14. Один выбранный case в настройках, свойствах и значениях по умолчанию
  15. Вариант перечисления как готовое правило расчёта
  16. Стабильный JSON и устаревшая сохранённая строка enum
  17. Функции и методы этой темы
  18. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 51 · От основ к архитектуре

Enum и readonly: допустимые варианты и неизменяемые значения

Моделируем статусы заказа, валидируем внешние строки и отделяем запрет переприсваивания от глубокой неизменяемости.

Выбрать один допустимый статус заказа

В одном месте написали paid, в другом — payed, и оплаченный заказ исчез из фильтра. Enum задаёт закрытый список вариантов: программа выбирает известный case, а не произвольную строку.

Что получится. Получим статус Оплачен из OrderStatus::Paid. Затем разберём внешний код и неизменяемую цену отдельно.

Как проходит конкретный вызов
  1. OrderStatus объявляет разрешённые случаи
  2. Заказ хранит конкретный case Paid
  3. match выбирает подпись для этого case
  4. Внешние строки переводятся в 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; сравнение показывает обычное использование объектов из примера.

Стабильный код paid и русская подписьСамостоятельный пример
<?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-свойство допускает инициализацию по правилам своей области, но запрещает обычную последующую запись. Тип обязателен. Оно не проверяет положительность цены: добавьте проверку конструктора, если цена приходит извне. Цена в заказе и сегодняшняя цена каталога могут различаться.

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.1Enum и readonly-свойства
PHP 8.2readonly class распространяет ограничения на свойства класса
PHP 8.3Повторная инициализация readonly внутри __clone по специальному правилу
PHP 8.4Область установки readonly по умолчанию protected(set), если не ограничена явно
PHP 8.5clone с массивом замен, в том числе 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 мы не записываем.

Пара существующих вариантов вместо новых объектов
  1. Объявлены Pickup и Courier
  2. suggestedFor(20) выбирает Courier
  3. label возвращает Курьер
  4. 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 от внутренней сериализации.

Один статус, разные внешние формы
  1. В PHP выбран ShipmentCode::Ready
  2. JSON получает скаляр ready
  3. Внутренняя сериализация запоминает конкретный case
  4. Восстановление возвращает тот же case, а не его клон
У backed enum есть код для JSON, у pure enum его нужно выбратьСамостоятельный пример
<?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 получает имя. Это продолжение первого перечисления; ошибки типов разобраны в объявлениях типов функции.

Один счёт, два представления состояния
  1. В хранилище находится число 20
  2. InvoiceState::from(20) находит Paid
  3. В PHP работаем с именованным case
  4. Для сохранения читаем 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-переменная.

Один выбор формата расходится по местам использования
  1. ReportFormat::Csv — существующий вариант
  2. DEFAULT_REPORT хранит тот же case
  3. Свойство и параметр получают этот вариант по умолчанию
  4. Name/value дают подпись и внешний код без создания нового case

Двоеточие string задаёт внешний текстовый код. Const хранит выбранный вариант, public объявляет свойство объекта, а static внутри функции сохраняет локальную переменную между вызовами. С PHP 8.2 name/value case можно читать в константных выражениях. Это разрешённое чтение свойств перечисления, а не общее разрешение вызывать методы в const.

Формат CSV остаётся одним вариантом в разных объявлениях — PHP 8.2+Самостоятельный пример
<?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 у обычного объекта: круглые скобки после объекта вызывают этот метод.

Срочный тариф обрабатывает стоимость товаров
  1. Выбран DeliveryTariff::Express
  2. calculateOrder получает case как callable
  3. Скобки вызывают __invoke с 1200 копейками
  4. Результат 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 показал стандартный формат; теперь меняем его осознанно.

Один статус имеет два разных внешних договора
  1. ShipmentPhase::Ready существует в PHP
  2. jsonSerialize формирует code и label для клиента
  3. serialize записывает имя PHP-класса и case
  4. Старое имя case после изменения программы может не восстановиться

Implements JsonSerializable обещает метод jsonSerialize. Его результат получает json_encode. Возвращаемый массив — форма ответа API; он не обязан совпадать с внутренним name. Unserialize может выдать предупреждение и вернуть false, если сохранённый case больше не существует. В опыте повреждаем только созданную нами локальную строку.

Явный JSON и обработка старого имени 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 при неподходящем типе с учётом режима преобразований.
Строгий поиск известного состояния
  1. Данные должны содержать известный код
  2. from(value) ищет совпадение
  3. Совпадение → 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.
Ожидаемая проверка пользовательского выбора
  1. Проверить тип входа
  2. tryFrom(value)
  3. 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, а не прячьте ошибку пустой строкой.

tryFrom не является универсальным валидатором mixedСамостоятельный пример
<?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 не добавляются в список.
Список вариантов для выбора
  1. cases() → [New, Paid]
  2. foreach получает один case
  3. 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 быть текстом интерфейса.

Обычная константа не становится ещё одним 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.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.