Статусы заказов на PHP: от простого к сложному

Раздел: Интернет-магазин -> Статусы заказов

Основные подходы к реализации статусов заказов

Каким образом реализовать статусы заказов с помощью перечислений enum в PHP 8.1+?

Начиная с PHP 8.1, появилась встроенная поддержка перечислений (enum). Это наиболее эффективный способ определить конечный набор статусов. Перечисление гарантирует, что в коде используются только допустимые значения, а IDE предоставляет автодополнение.


<?php

enum OrderStatus: int
{
    case New = 0;
    case Processing = 1;
    case Shipped = 2;
    case Delivered = 3;
    case Cancelled = 4;

    public function label(): string
    {
        return match($this) {
            self::New => 'Новый',
            self::Processing => 'В обработке',
            self::Shipped => 'Отправлен',
            self::Delivered => 'Доставлен',
            self::Cancelled => 'Отменён',
        };
    }

    public static function fromInt(int $value): self
    {
        return self::tryFrom($value) ?? throw new \InvalidArgumentException('Неизвестный статус');
    }
}
?>
  

Orders status php (статус заказов php)

Поле статуса в таблице заказов хранится в виде целого числа (int). При получении заказа значение приводится к enum через OrderStatus::fromInt($status).

Цель: обеспечить типобезопасность и читаемость кода. Подходит для проектов с фиксированным набором статусов, которые редко меняются.

Случаи использования: интернет-магазины, где статусы не добавляются динамически (например, через админку).

Типичная проблема: при попытке присвоить статус, не входящий в enum, выбрасывается исключение. Решение: всегда проверять входные данные через tryFrom() или валидацию на уровне формы.

Ещё одна сложность: enum нельзя сериализовать в JSON без дополнительной обработки. Решение: добавить метод jsonSerialize() или использовать атрибуты, которые возвращают метку, а не числовой код.

Как использовать константы класса для определения статусов заказов без enum?

Если версия PHP ниже 8.1 или проект предпочитает статическую инициализацию, применяют константы класса. Этот подход популярен в старых фреймворках.


<?php

class OrderStatus
{
    const NEW = 0;
    const PROCESSING = 1;
    const SHIPPED = 2;
    const DELIVERED = 3;
    const CANCELLED = 4;

    private static $labels = [
        self::NEW => 'Новый',
        self::PROCESSING => 'В обработке',
        self::SHIPPED => 'Отправлен',
        self::DELIVERED => 'Доставлен',
        self::CANCELLED => 'Отменён',
    ];

    public static function getLabel(int $status): string
    {
        return self::$labels[$status] ?? 'Неизвестный';
    }

    public static function isValid(int $status): bool
    {
        return array_key_exists($status, self::$labels);
    }
}
?>
  

Цель: быстрое внедрение без изменения используемой версии PHP. Подходит для легаси-проектов.

Случаи использования: проекты на PHP 7.4 и ниже, где нельзя установить enum.

Типичная ошибка: опечатки в именах констант из-за отсутствия автодополнения. Решение: использовать статические методы с проверкой.

Неудобство: приходится вручную синхронизировать массив меток. Решение: генерировать массив через рефлексию, но это усложняет код.

Как хранить статусы в отдельной таблице и связывать их с заказами?

Для динамического управления статусами (например, добавление новых через админку) лучше использовать отдельную таблицу order_statuses со ссылкой из таблицы orders.


-- SQL схема
CREATE TABLE order_statuses (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    sort_order INT DEFAULT 0
);

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    status_id INT,
    FOREIGN KEY (status_id) REFERENCES order_statuses(id)
);
  

<?php

class OrderRepository
{
    private $pdo;

    public function getOrdersWithStatus(): array
    {
        $sql = "SELECT o.id, o.status_id, s.name AS status_name
                FROM orders o
                JOIN order_statuses s ON o.status_id = s.id";
        return $this->pdo->query($sql)->fetchAll();
    }
}
?>
  

Цель: гибкость – статусы можно добавлять без изменения кода. Удобно для CMS и многоязычных магазинов.

Случаи использования: системы, где администратор может создавать собственные статусы (например, «Возврат оформлен», «Ожидание оплаты»).

Проблема: потеря типобезопасности – в коде появляются магические числа или названия. Решение: создать класс-перечисление на основе данных из таблицы, например, с помощью Doctrine Repository.

Частая ошибка: забывают про связь внешнего ключа, что приводит к ошибкам при изменении статусов. Решение: использовать транзакции и валидацию на уровне приложения.

Как внедрить конечный автомат (State Machine) для управления переходами статусов?

Когда порядок смены статусов строго регламентирован (например, из «Нового» можно перейти только в «В обработку» или «Отмену», но не сразу в «Доставлен»), помогает конечный автомат. Рассмотрим пример с использованием библиотеки ato/php-state-machine.


<?php

use Ato\StateMachine\StateMachine;
use Ato\StateMachine\Transition;

$machine = new StateMachine(
    states: ['new', 'processing', 'shipped', 'delivered', 'cancelled'],
    transitions: [
        new Transition('process', 'new', 'processing'),
        new Transition('ship', 'processing', 'shipped'),
        new Transition('deliver', 'shipped', 'delivered'),
        new Transition('cancel', 'new', 'cancelled'),
        new Transition('cancel_from_processing', 'processing', 'cancelled'),
    ]
);

// Попытка перехода
if ($machine->can($orderStatus, 'ship')) {
    $newStatus = $machine->apply($orderStatus, 'ship');
}
?>
  

Цель: исключить недопустимые переходы и сделать логику статусов прозрачной. Подходит для сложных бизнес-процессов.

Случаи использования: доставка, логистика, многоэтапная обработка заказов.

Проблема: переусложнение для простых наборов статусов. Решение: использовать только при явной необходимости.

Ошибка: забывают включить все возможные переходы – тогда статус «застревает». Решение: тестировать все сценарии и добавить общий fallback-переход (например, отмена на любом этапе).

Расширенные примеры реализации статусов заказов

Помимо базовых вариантов, рассмотрим полноценный менеджер статусов с транзакциями, логированием и правами доступа. Пример использует enum и PDO.

Пример

<?php

declare(strict_types=1);

class OrderStatusManager
{
    private PDO $pdo;
    private Logger $logger;

    public function __construct(PDO $pdo, Logger $logger)
    {
        $this->pdo = $pdo;
        $this->logger = $logger;
    }

    /**
     * Обновить статус заказа с проверкой перехода и записью лога.
     *
     * @param int $orderId
     * @param OrderStatus $newStatus
     * @param string $changedBy
     * @return bool
     * @throws \RuntimeException
     */
    public function updateStatus(int $orderId, OrderStatus $newStatus, string $changedBy): bool
    {
        $this->pdo->beginTransaction();
        try {
            // Получаем текущий статус
            $stmt = $this->pdo->prepare('SELECT status FROM orders WHERE id = :id FOR UPDATE');
            $stmt->execute([':id' => $orderId]);
            $row = $stmt->fetch();
            if (!$row) {
                throw new \RuntimeException('Заказ не найден');
            }
            $currentStatus = OrderStatus::fromInt((int)$row['status']);

            // Проверяем допустимость перехода (пример: только вперед по числовому значению)
            if ($newStatus->value <= $currentStatus->value) {
                throw new \RuntimeException('Недопустимый переход статуса');
            }

            // Обновляем статус
            $stmt = $this->pdo->prepare('UPDATE orders SET status = :status WHERE id = :id');
            $stmt->execute([':status' => $newStatus->value, ':id' => $orderId]);

            // Логируем изменение
            $this->logger->log([
                'order_id' => $orderId,
                'old_status' => $currentStatus->name,
                'new_status' => $newStatus->name,
                'changed_by' => $changedBy,
                'changed_at' => date('Y-m-d H:i:s'),
            ]);

            $this->pdo->commit();
            return true;
        } catch (\Throwable $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    /**
     * Получить список доступных для перехода статусов.
     */
    public function getAvailableStatuses(OrderStatus $current): array
    {
        $all = OrderStatus::cases();
        return array_filter($all, fn(OrderStatus $s) => $s->value > $current->value);
    }
}
?>

Результат выполнения (пример лога):

Array
(
    [order_id] => 123
    [old_status] => New
    [new_status] => Processing
    [changed_by] => admin@example.com
    [changed_at] => 2025-03-01 14:35:12
)

Ещё один расширенный пример – интеграция с Symfony Form. Используем ChoiceType с перечислением.

Пример

// в Form/OrderType.php
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        ->add('status', ChoiceType::class, [
            'choices' => array_combine(
                array_map(fn(OrderStatus $s) => $s->label(), OrderStatus::cases()),
                OrderStatus::cases()
            ),
            'choice_value' => fn(?OrderStatus $status) => $status?->value,
        ]);
}

Результат: пользователь видит выпадающий список с русскими названиями статусов, а форма сохраняет числовое значение.

Для ещё более гибкой системы можно добавить асинхронное обновление через AJAX с проверкой прав доступа:

Пример

// controller/ajax.php
public function changeStatusAction(Request $request): JsonResponse
{
    if (!$this->isGranted('ROLE_ADMIN')) {
        return new JsonResponse(['error' => 'Доступ запрещён'], 403);
    }
    $orderId = $request->request->getInt('order_id');
    $newStatusValue = $request->request->getInt('status');
    $newStatus = OrderStatus::tryFrom($newStatusValue);
    if (!$newStatus) {
        return new JsonResponse(['error' => 'Неверный статус'], 400);
    }
    try {
        $this->orderManager->updateStatus($orderId, $newStatus, $this->getUser()->getUsername());
        return new JsonResponse(['success' => true]);
    } catch (\Throwable $e) {
        return new JsonResponse(['error' => $e->getMessage()], 500);
    }
}

Результат: в админке можно менять статус без перезагрузки страницы.

статус заказов PHP - comments

En
Orders status php (php)