joetannenbaum/chewie
Chewie generates a fake REST API from your Laravel Eloquent models, giving front-end and mobile developers realistic endpoints to prototype against. Relationships, pagination, and filtering work out of the box, with no custom controllers or seed data required.
Chewie is a small trait-based state machine for Eloquent models. There is no service provider to register and no config file to publish — installation is a single Composer require:
composer require joetannenbaum/chewie
Add the trait to any model and declare a $stateMachine array describing the attribute, its initial value, and the allowed transitions:
use Joetannenbaum\Chewie\HasStateMachine;
class Order extends Model
{
use HasStateMachine;
public $stateMachine = [
'status' => [
'initial' => 'pending',
'transitions' => [
'pay' => ['pending' => 'paid'],
'ship' => ['paid' => 'shipped'],
'cancel' => [
'pending' => 'cancelled',
'paid' => 'cancelled',
],
],
],
];
}
The transitions map is from => to; listing several from keys under one transition name (as with cancel above) lets a single verb apply from multiple states.
Your first use case is almost always an order/invoice status column:
$order = Order::create(); // status = 'pending'
$order->canTransition('pay'); // true
$order->transition('pay'); // status = 'paid', persisted
$order->canTransition('pay'); // false
Where to look first: the README on the GitHub repo (it is short), then src/HasStateMachine.php — the whole package is a few hundred lines, so reading the trait once tells you exactly which methods exist, whether transition() saves for you, and what it does on an illegal move. Do that before designing around it; the README is thinner than the code.
Guard at the boundary, not in the view. Put the check in the controller or a form request so an invalid transition is a 422/409, not a silent no-op:
public function pay(Order $order)
{
abort_unless($order->canTransition('pay'), 409, 'Order cannot be paid in its current state.');
$order->transition('pay');
return back();
}
Wrap transitions in an action class once side effects appear. Chewie moves an attribute; it does not send receipts or write ledger rows. A thin TransitionOrderStatus action that calls transition() and then dispatches your own domain event keeps the trait from becoming your business logic:
class TransitionOrderStatus
{
public function __invoke(Order $order, string $transition): Order
{
DB::transaction(function () use ($order, $transition) {
$order->transition($transition);
OrderStatusChanged::dispatch($order, $transition);
});
return $order->refresh();
}
}
Drive UI from the machine, not from hardcoded arrays. Expose the legal moves so Blade/Vue buttons stay in sync with the definition:
$available = collect(array_keys($order->stateMachine['status']['transitions']))
->filter(fn ($t) => $order->canTransition($t));
Cast the column deliberately. Add status to $casts if you use a PHP enum; otherwise treat it as a plain string. Don't half-migrate — pick strings or backed enums and use them consistently in the transitions map.
Test the machine, not the framework. A data-provider test over every transition pair (including the illegal ones) is cheap and catches config typos:
it('only pays from pending', function () {
$order = Order::factory()->create(['status' => 'paid']);
expect($order->canTransition('pay'))->toBeFalse();
});
Multiple machines per model work by adding more keys to $stateMachine (e.g. status and fulfillment_status), which is handy for order workflows where shipping and payment move independently.
transition() vs transitionTo(), whether it saves automatically, and what it returns/thows on an illegal move are the three things that bite people. The docs are sparse; src/HasStateMachine.php is the source of truth. Pin the version you validated against in composer.json.canTransition() and both call transition(). For anything money-adjacent, wrap in DB::transaction() with lockForUpdate() or an optimistic where('status', $from)->update(...) guard.NULL status won't be backfilled — set a column default in the migration too, or your first canTransition() on legacy rows will be confusing.updated fires because the attribute is dirty, but there is no Transitioning/Transitioned hook unless your version dispatches them. Don't assume observers will catch the semantic transition; dispatch your own event from the wrapper action.$stateMachine off the model; if you already have a property with that name or declare it private, you'll get silent misbehaviour rather than an error. public is the safest choice.false or null rather than throw. Always check canTransition() first and treat a falsy return as a bug in your own code, not a user error.where('status', 'paid') is what you write. If you find yourself repeating state lists in queries, add local scopes on the model rather than reaching into the machine array from controllers.canTransition() to add authorisation checks, or keep guards entirely outside the trait in your action layer. The latter ages better.How can I help you explore Laravel packages today?