Статус замовлення, заявки чи статті рідко може змінитися на будь-який інший: оплачене не стає новим, відправлене не скасовується. Ці правила - машина станів, і enum - зручне місце, щоб описати її в одному місці.
enum OrderStatus: string
{
case New = 'new';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
/** @return list<self> */
public function allowedTransitions(): array
{
return match ($this) {
self::New => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped, self::Cancelled => [],
};
}
public function canTransitionTo(self $next): bool
{
return in_array($next, $this->allowedTransitions(), true);
}
}
Застосування в моделі:
public function transitionTo(OrderStatus $next): void
{
if (! $this->status->canTransitionTo($next)) {
throw new InvalidStateTransition($this->status, $next);
}
$this->update(['status' => $next]);
event(new OrderStatusChanged($this, $next));
}
Що це дає:
- Правила переходів - в одному місці, а не розкидані
if-ами по контролерах. matchбезdefaultзмушує описати переходи для кожного нового статусу.- Легко тестувати: таблиця «з якого - в який - дозволено».
- UI може показувати лише доступні дії:
$order->status->allowedTransitions().
Коли enum уже замало: переходи залежать від даних (оплатити можна, лише якщо сума збігається), потрібні дії при вході й виході зі стану, історія переходів, паралельні стани. Тоді - окремі класи станів (патерн State) чи бібліотеки на кшталт spatie/laravel-model-states, а для довгих бізнес-процесів - workflow-рушії.
Конкурентність: перевірка й оновлення мають бути атомарними - UPDATE ... SET status = 'paid' WHERE id = ? AND status = 'new' чи блокування рядка, інакше два паралельні запити обидва «побачать» статус New.