PHPackages                             fsmonter/machina - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. fsmonter/machina

ActiveLibrary[Database &amp; ORM](/categories/database)

fsmonter/machina
================

Fluent enum-powered state machines for Laravel

v0.1.0(1mo ago)11↑2900%MITPHPPHP ^8.2CI passing

Since Mar 28Pushed 1mo agoCompare

[ Source](https://github.com/fsmonter/machina)[ Packagist](https://packagist.org/packages/fsmonter/machina)[ Docs](https://github.com/fsmonter/machina)[ RSS](/packages/fsmonter-machina/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (8)Versions (2)Used By (0)

Machina
=======

[](#machina)

Enum-powered state machines for Laravel.

Installation
------------

[](#installation)

```
composer require fsmonter/machina
```

Quick Start
-----------

[](#quick-start)

### 1. Define states as a backed enum

[](#1-define-states-as-a-backed-enum)

```
enum OrderState: string
{
    case Pending = 'pending';
    case Processing = 'processing';
    case Completed = 'completed';
    case Failed = 'failed';
    case Cancelled = 'cancelled';
}
```

### 2. Define a state machine

[](#2-define-a-state-machine)

```
use Machina\Machina;
use Machina\StateBuilder;
use Machina\StateMachineBuilder;

class OrderStateMachine extends Machina
{
    public function transitions(): StateMachineBuilder
    {
        return machina()
            ->initial(OrderState::Pending)
            ->state(OrderState::Pending, function (StateBuilder $state) {
                $state->on('process')
                    ->target(OrderState::Processing)
                    ->guard(fn (Order $order) => $order->total > 0)
                    ->action(fn (Order $order) => $order->notify(new OrderProcessing));
                $state->on('cancel')->target(OrderState::Cancelled);
            })
            ->state(OrderState::Processing, function (StateBuilder $state) {
                $state->on('complete')
                    ->target(OrderState::Completed)
                    ->action(fn (Order $order) => $order->update(['completed_at' => now()]));
                $state->on('fail')->target(OrderState::Failed);
            })
            ->final(OrderState::Completed, OrderState::Failed, OrderState::Cancelled);
    }
}
```

### 3. Add the trait to your model

[](#3-add-the-trait-to-your-model)

```
use Machina\HasStateMachine;

class Order extends Model
{
    use HasStateMachine;

    protected $stateMachines = [
        'state' => OrderStateMachine::class,
    ];
}
```

### 4. Use it

[](#4-use-it)

```
$order = Order::create(); // state column is auto-set to 'pending'

// Get current state enum
$order->state->current(); // OrderState::Pending

// Send operations by name
$order->state->send('process');

// Or use magic methods
$order->state->process();

// Check if an operation is available
$order->state->canSend('complete');  // true
$order->state->canComplete();        // true

// List all available operations from the current state
$order->state->availableOperations(); // ['complete', 'fail']
```

Initial State
-------------

[](#initial-state)

Define an initial state with `initial()`. When a model is created without an explicit state value, the initial state is set automatically:

```
Order::create();                                // state = OrderState::Pending
Order::create(['state' => OrderState::Failed]); // state = OrderState::Failed (not overridden)
```

Operations
----------

[](#operations)

Operations are the primary way to interact with state machines. They let you declare intent ("approve this transaction") rather than specify target states ("go to approved").

```
machina()
    ->initial(TransactionState::Pending)
    ->state(TransactionState::Pending, function (StateBuilder $state) {
        $state->on('approve')
            ->target(TransactionState::Approved)
            ->guard(fn (Transaction $tx) => $tx->amount account->balance)
            ->action(fn (Transaction $tx) => $tx->account->debit($tx->amount));
        $state->on('reject')
            ->target(TransactionState::Rejected)
            ->action(fn (Transaction $tx) => $tx->notify(new TransactionRejected));
        $state->on('flag')
            ->action(fn (Transaction $tx) => $tx->notifyCompliance());
    })
    ->state(TransactionState::Approved, function (StateBuilder $state) {
        $state->on('settle')
            ->target(TransactionState::Settled)
            ->guard(fn (Transaction $tx) => $tx->cleared_at !== null);
    })
    ->state(TransactionState::Settled, function (StateBuilder $state) {
        $state->on('reverse')
            ->target(TransactionState::Reversed)
            ->action(fn (Transaction $tx) => $tx->account->credit($tx->amount));
    })
    ->final(TransactionState::Settled, TransactionState::Rejected, TransactionState::Reversed)
```

**`state(BackedEnum $state, Closure $callback)`** groups operations by their source state.

Inside the closure, define operations with:

- **`$state->on(string $name)`** starts an operation definition
- **`->target(BackedEnum $state)`** sets the target state
- **`->guard(Closure|array $guard)`** adds condition(s) that must pass
- **`->action(Closure $action)`** runs after the transition completes

### Execution order

[](#execution-order)

For operations with a target: guard evaluation, atomic state transition, then `action` closure.

For state-bound operations (no `target()`): guard evaluation, then `action` closure. The state does not change.

Note: `action` runs after the database transaction commits. If the closure throws, the state change is already persisted.

### State-bound operations

[](#state-bound-operations)

An operation without `target()` runs its `action` closure without changing state. Useful for actions that belong to a specific state but don't trigger a transition:

```
->state(OrderState::Processing, function (StateBuilder $state) {
    $state->on('sendUpdate')
        ->action(fn (Order $order) => $order->notifyCustomer());
})
```

Direct Transitions
------------------

[](#direct-transitions)

For simple state machines without named operations, use `transition()`:

```
machina()
    ->initial(OrderState::Pending)
    ->transition(from: OrderState::Pending, to: OrderState::Processing)
    ->transition(from: OrderState::Pending, to: OrderState::Cancelled)
    ->transition(from: OrderState::Processing, to: OrderState::Completed,
        guard: fn (Order $order) => $order->isPaid())
    ->transition(from: OrderState::Processing, to: OrderState::Failed)
    ->final(OrderState::Completed, OrderState::Failed, OrderState::Cancelled)
```

You can also use direct transitions on the model:

```
$order->state->transitionTo(OrderState::Processing);
$order->state->canTransitionTo(OrderState::Processing); // bool
$order->state->allowedTransitions(); // [OrderState::Processing, OrderState::Cancelled]
```

`state()` and `transition()` can be mixed on the same builder.

Transition Guards
-----------------

[](#transition-guards)

Guards add conditions to transitions. All guards must pass for the transition to proceed:

```
->transition(from: OrderState::Pending, to: OrderState::Processing,
    guard: [
        fn (Order $order) => $order->total > 0,
        fn (Order $order) => $order->items()->exists(),
    ])
```

On operations, guards are chained:

```
->state(OrderState::Pending, function (StateBuilder $state) {
    $state->on('process')
        ->target(OrderState::Processing)
        ->guard(fn (Order $order) => $order->total > 0);
})
```

When a guard fails, `canTransitionTo()` / `canSend()` returns false and `transitionTo()` / `send()` throws `InvalidStateTransitionException`.

Events
------

[](#events)

A `StateTransitioned` event fires after every successful transition:

```
use Machina\Events\StateTransitioned;

class HandleOrderTransition
{
    public function handle(StateTransitioned $event): void
    {
        $event->model;    // The Eloquent model
        $event->oldState; // Previous state (BackedEnum)
        $event->newState; // New state (BackedEnum)
    }
}
```

To use a custom event class, override `eventClass()` in your state machine:

```
protected function eventClass(): string
{
    return OrderStateChanged::class;
}
```

Additional Data
---------------

[](#additional-data)

Pass extra columns to update alongside the state transition:

```
$order->state->transitionTo(OrderState::Processing, [
    'processed_at' => now(),
    'processor_id' => auth()->id(),
]);

// Also works with send()
$order->state->send('process', [
    'processed_at' => now(),
]);
```

State Introspection
-------------------

[](#state-introspection)

```
$order->state->current();         // OrderState::Pending (the enum)
$order->state->is(OrderState::Pending); // true
$order->state->isFinal();       // false
$order->state->stateMachine();  // StateMachine instance
(string) $order->state;         // "pending"
```

Integer Backed Enums
--------------------

[](#integer-backed-enums)

Both string and integer backed enums are supported:

```
enum Priority: int
{
    case Low = 0;
    case Medium = 1;
    case High = 2;
}
```

Concurrency Safety
------------------

[](#concurrency-safety)

Transitions use atomic database updates with a WHERE clause on the current state, wrapped in a database transaction using the model's own connection. If the state was modified between reading and writing, an `InvalidStateTransitionException` is thrown.

Note: Eloquent model events (`saving`, `updated`, etc.) are not fired during transitions. The raw query is intentional for atomicity.

License
-------

[](#license)

MIT

###  Health Score

35

—

LowBetter than 79% of packages

Maintenance90

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b457dd3c36eb56eb2ba54e83ba1eb5d608bfa55c2555ccf87b69184fd0d24c9e?d=identicon)[fsmonter](/maintainers/fsmonter)

---

Top Contributors

[![fsmonter](https://avatars.githubusercontent.com/u/34519027?v=4)](https://github.com/fsmonter "fsmonter (50 commits)")

---

Tags

laravelenumeloquentstatephp-enumstate-machine

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/fsmonter-machina/health.svg)

```
[![Health](https://phpackages.com/badges/fsmonter-machina/health.svg)](https://phpackages.com/packages/fsmonter-machina)
```

###  Alternatives

[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k7.2M71](/packages/mongodb-laravel-mongodb)[watson/validating

Eloquent model validating trait.

9723.3M47](/packages/watson-validating)[cybercog/laravel-ban

Laravel Ban simplify blocking and banning Eloquent models.

1.1k651.8k11](/packages/cybercog-laravel-ban)[dyrynda/laravel-model-uuid

This package allows you to easily work with UUIDs in your Laravel models.

4802.8M8](/packages/dyrynda-laravel-model-uuid)[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k4.8M26](/packages/tucker-eric-eloquentfilter)[cybercog/laravel-love

Make Laravel Eloquent models reactable with any type of emotions in a minutes!

1.2k302.7k1](/packages/cybercog-laravel-love)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
