Installation:
composer require norotaro/enumata
Ensure your Laravel version is ≥9.x (check composer.json dependencies).
Define a State Machine:
Create an enum class (e.g., app/Enums/OrderStatus.php) extending Enumata\StateDefinition:
namespace App\Enums;
use Enumata\StateDefinition;
class OrderStatus extends StateDefinition
{
public static function states(): array
{
return [
'draft' => ['can' => ['submit']],
'submitted' => ['can' => ['approve', 'reject']],
'approved' => ['can' => ['cancel']],
'rejected' => ['can' => []],
'cancelled' => ['can' => []],
];
}
}
Configure the Model:
In your Eloquent model (e.g., app/Models/Order.php), add:
use Enumata\HasStates;
class Order extends Model
{
use HasStates;
protected $stateEnum = OrderStatus::class;
protected $stateColumn = 'status';
}
First Use Case: Transition a model instance:
$order = Order::find(1);
$order->transition('submit'); // Validates and updates state
State Validation:
Use canTransition() to check allowed transitions before calling transition():
if ($order->canTransition('approve')) {
$order->transition('approve');
}
Bulk State Updates:
Leverage updateStates() for batch operations (e.g., cron jobs):
Order::where('status', 'submitted')
->updateStates('approve'); // Requires `force: true` in config
Dynamic State Logic:
Override getAllowedTransitions() in the model for context-aware rules:
public function getAllowedTransitions(string $state): array
{
$transitions = parent::getAllowedTransitions($state);
if ($state === 'approved' && $this->isHighPriority()) {
$transitions[] = 'fast_track';
}
return $transitions;
}
Event-Driven Transitions:
Hook into stateChanging and stateChanged events:
protected static function booted()
{
static::stateChanging(function ($model, $from, $to) {
// Log or validate pre-transition
});
static::stateChanged(function ($model, $from, $to) {
// Send notifications, update related models
});
}
API Responses: Return current state in JSON:
return response()->json([
'order' => $order,
'status' => $order->getState(),
'allowed_actions' => $order->getAllowedTransitions(),
]);
Admin Panels:
Use getAllowedTransitions() to dynamically render UI buttons (e.g., with Livewire/Inertia).
Testing: Mock state transitions in unit tests:
$order->shouldReceive('transition')->once()->with('approve');
$order->transition('approve');
Column Mismatch:
Ensure $stateColumn matches the database column name exactly (case-sensitive).
Fix: Verify with Schema::getColumnListing('orders').
Circular Dependencies:
Avoid defining states that create loops (e.g., A → B → A).
Fix: Use can arrays to explicitly block invalid paths.
Mass Assignment Risks:
Never use fill() or update() directly on the state column. Always use transition().
Fix: Add $guarded = ['status'] to your model.
Enum Caching: State definitions are cached. Clear config after changes:
php artisan config:clear
Transition Errors: Check the exception message for invalid transitions. Enable debug mode:
config(['enumata.debug' => true]);
Logs will show attempted transitions and allowed states.
State Not Found:
Verify the enum class is autoloaded (check composer dump-autoload).
Custom Guards:
Extend Enumata\StateDefinition to add logic:
public static function canTransition($from, $to): bool
{
if ($from === 'draft' && now()->isAfter('2023-12-31')) {
return false; // Expired drafts can't be submitted
}
return parent::canTransition($from, $to);
}
State Metadata: Attach data to states (e.g., for UI hints):
public static function states(): array
{
return [
'draft' => [
'can' => ['submit'],
'meta' => ['color' => 'gray', 'label' => 'Draft'],
],
];
}
Database Constraints: Add a foreign key constraint to enforce valid states:
Schema::table('orders', function (Blueprint $table) {
$table->string('status')
->comment('Valid values: draft, submitted, approved, rejected, cancelled');
});
Force Transitions:
Enable force_transitions in config/enumata.php to bypass validation (use sparingly):
'force_transitions' => env('ENUMATA_FORCE_TRANSITIONS', false),
Warning: Only use for admin overrides or migrations.
Nullable States:
Set nullable: true in the state definition to allow null values:
return [
null => ['can' => ['initialize']],
// ...
];
How can I help you explore Laravel packages today?