Статусы заказов на 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);
}
}
Результат: в админке можно менять статус без перезагрузки страницы.