<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
soylentgreenstudio / laravel-enum-states example snippets
// 1. Define your enum with attributes
enum OrderStatus: string
{
#[InitialState]
#[Transition(to: [self::Processing])]
case Pending = 'pending';
#[Transition(to: [self::Shipped])]
case Processing = 'processing';
#[FinalState]
case Shipped = 'shipped';
// Reachable from any non-final case — no need to list it on each source
#[FinalState]
#[TransitionFrom(from: '*')]
case Cancelled = 'cancelled';
}
// 2. Add the trait to your model — that's it
class Order extends Model
{
use HasStateMachines;
protected $casts = [
'status' => OrderStatus::class,
];
}
// 3. Transition states
$order->transitionTo(OrderStatus::Processing);
$order->transitionTo(OrderStatus::Shipped, ['tracking' => 'ABC123']);
// 4. Check if transition is allowed (never throws)
$order->canTransitionTo(OrderStatus::Cancelled); // bool
// 4b. Or list every state reachable right now, guards applied
$order->allowedTransitions(); // [OrderStatus::Shipped, OrderStatus::Cancelled]
// 5. Query by state
Order::whereState('status', OrderStatus::Pending)->get();
// 6. View transition history
$order->stateHistory('status');
use SoylentGreenStudio\EnumStates\Attributes\InitialState;
use SoylentGreenStudio\EnumStates\Attributes\FinalState;
use SoylentGreenStudio\EnumStates\Attributes\Transition;
use SoylentGreenStudio\EnumStates\Attributes\TransitionFrom;
enum OrderStatus: string
{
#[InitialState]
#[Transition(
to: [self::Processing],
guard: HasItemsInStock::class,
after: SendOrderConfirmation::class,
)]
case Pending = 'pending';
#[Transition(
to: [self::Shipped],
before: ValidateShippingAddress::class,
)]
case Processing = 'processing';
#[FinalState]
case Shipped = 'shipped';
// Reverse-declared: reachable from any non-final case
#[FinalState]
#[TransitionFrom(from: '*')]
case Cancelled = 'cancelled';
}
use SoylentGreenStudio\EnumStates\Traits\HasStateMachines;
class Order extends Model
{
use HasStateMachines;
protected $casts = [
'status' => OrderStatus::class,
'payment_status' => PaymentStatus::class,
];
}
enum OrderStatus: string
{
#[InitialState]
#[Transition(to: [self::Processing])]
case Pending = 'pending';
#[Transition(to: [self::Shipped])]
case Processing = 'processing';
#[FinalState]
case Shipped = 'shipped';
// Cancelled is reachable from Pending and Processing —
// without touching those cases' own attribute lists.
#[FinalState]
#[TransitionFrom(from: '*')]
case Cancelled = 'cancelled';
}
enum DocumentStatus: string
{
#[InitialState]
case Draft = 'draft';
case Review = 'review';
case Approved = 'approved';
// Archived from Draft or Review — but NOT from Approved
#[FinalState]
#[TransitionFrom(from: [self::Draft, self::Review])]
case Archived = 'archived';
}
use SoylentGreenStudio\EnumStates\Contracts\TransitionGuard;
class HasItemsInStock implements TransitionGuard
{
public function allow(Model $model, array $metadata): bool
{
return $model->items()->where('in_stock', true)->exists();
}
}
class HasSufficientBalance implements TransitionGuard
{
public function __construct(
private PaymentGateway $gateway,
) {}
public function allow(Model $model, array $metadata): bool
{
return $this->gateway->getBalance($model->user_id) >= $model->total;
}
}
#[Transition(
to: [self::Approved],
guard: [IsAdmin::class, HasApprovalPermission::class, BudgetAvailable::class],
)]
case Pending = 'pending';
use SoylentGreenStudio\EnumStates\Contracts\TransitionHook;
class SendOrderConfirmation implements TransitionHook
{
public function handle(Model $model, mixed $from, mixed $to, array $metadata): void
{
Mail::to($model->user)->send(new OrderConfirmed($model));
}
}
class StampShippedAt implements TransitionHook
{
public function handle(Model $model, mixed $from, mixed $to, array $metadata): void
{
$model->shipped_at = now(); // persisted with the transition
}
}
class NotifySlack implements TransitionHook
{
public function __construct(
private SlackClient $slack,
) {}
public function handle(Model $model, mixed $from, mixed $to, array $metadata): void
{
$this->slack->send("Order #{$model->id} changed from {$from->name} to {$to->name}");
}
}
// History for a specific field
$order->stateHistory('status');
// => Collection of StateTransition models
// History for all state machine fields
$order->stateHistory();
$orders = Order::with('stateTransitions')->get();
foreach ($orders as $order) {
$order->stateHistory('status'); // no extra queries — filtered in memory
}
// Exact match
Order::whereState('status', OrderStatus::Pending)->get();
// Exclude a state
Order::whereNotState('status', OrderStatus::Cancelled)->get();
// Match multiple states
Order::whereStateIn('status', [
OrderStatus::Pending,
OrderStatus::Processing,
])->get();
// In EventServiceProvider or via Event::listen()
use SoylentGreenStudio\EnumStates\Events\TransitionCompleted;
Event::listen(TransitionCompleted::class, function (TransitionCompleted $event) {
Log::info("Order #{$event->model->id}: {$event->field} changed", [
'from' => $event->from->value,
'to' => $event->to->value,
'meta' => $event->metadata,
]);
});
use SoylentGreenStudio\EnumStates\Contracts\AsyncTransitionHook;
class SendShipmentNotification implements AsyncTransitionHook
{
public function handle(Model $model, mixed $from, mixed $to, array $metadata): void
{
Mail::to($model->user)->send(new OrderShipped($model));
}
public function queue(): ?string
{
return 'notifications'; // or null for default queue
}
}
#[Transition(
to: [self::Shipped],
before: ValidateShippingAddress::class, // sync — runs inside transaction
after: SendShipmentNotification::class, // async — dispatched to queue
)]
case Processing = 'processing';
interface AsyncTransitionHook
{
public function handle(Model $model, mixed $from, mixed $to, array $metadata): void;
public function queue(): ?string; // queue name or null for default
}