PHP code example of stateflow / engine

1. Go to this page and download the library: Download stateflow/engine library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

stateflow / engine example snippets


// ❌ Without StateFlow - scattered, error-prone
if ($order->status === 'pending' && $action === 'pay') {
    $order->status = 'paid';
    // Hope you remembered to send the email...
    // Hope you remembered to log it...
    // Hope no other code sets status to something invalid...
}

// ✅ With StateFlow - clean, auditable, testable
$machine->apply($order, 'pay');



final class OrderStateDefinition
{
    public const STATES = [
        'pending',
        'paid',
        'shipped',
        'cancelled',
    ];

    public const TRANSITIONS = [
        'pay' => [
            'from' => ['pending'],
            'to' => 'paid',
        ],
        'ship' => [
            'from' => ['paid'],
            'to' => 'shipped',
        ],
        'cancel' => [
            'from' => ['pending', 'paid'],
            'to' => 'cancelled',
        ],
    ];
}



use Stateflow\Engine\StateMachine;

$machine = StateMachine::create(OrderStateDefinition::class);

// Your subject (from your database, or anywhere)
$order = Order::find(1); // $order->status is 'pending'

// Apply a transition
$machine->apply($order, 'pay'); // $order->status is now 'paid'

// Save to your database
$order->save();

public const STATES = ['draft', 'pending', 'approved', 'rejected'];

public const TRANSITIONS = [
    'submit' => [
        'from' => ['draft'],      // Can only submit from draft
        'to' => 'pending',
    ],
    'approve' => [
        'from' => ['pending'],    // Can only approve from pending
        'to' => 'approved',
    ],
    'reject' => [
        'from' => ['pending', 'approved'],  // Can reject from multiple states
        'to' => 'rejected',
    ],
];

// All of these work:
$machine->apply($order, 'pay');           // Object
$machine->apply($data, 'pay');            // Array
$machine->apply($eloquentModel, 'pay');   // Eloquent

$machine = StateMachine::create(
    definition: OrderStateDefinition::class,  // Required
    workflow: OrderWorkflow::class,           // Optional: guards and hooks
    accessor: new CustomAccessor(),           // Optional: custom state access
    field: 'status',                          // Optional: default is 'status'
    initialState: 'pending',                  // Optional: for initialize()
);

// Apply a transition (throws if invalid)
$machine->apply($subject, 'pay');

// Check if transition is possible
if ($machine->can($subject, 'pay')) {
    $machine->apply($subject, 'pay');
}

// Get current state
$state = $machine->getState($subject); // 'pending'

// Get available transitions from current state
$transitions = $machine->getAvailableTransitions($subject); // ['pay', 'cancel']

$machine = StateMachine::create(
    definition: OrderStateDefinition::class,
    initialState: 'pending',
);

$order = new Order(); // status is null
$machine->initialize($order); // status is now 'pending'

// Get all states
$machine->getStates(); // ['pending', 'paid', 'shipped', 'cancelled']

// Get all transitions
$machine->getTransitions(); // ['pay' => [...], 'ship' => [...], ...]

// Export as array (JSON-serializable)
$machine->toArray();

// Export as Mermaid diagram
echo $machine->toMermaid();

// Validate without creating (returns errors array)
$errors = StateMachine::validate(OrderStateDefinition::class);

if (empty($errors)) {
    echo "Definition is valid!";
}



use Stateflow\Engine\Attribute\Guard;

final class OrderWorkflow
{
    #[Guard('pay')]
    public function hasValidAmount(
        object|array $subject,
        string $transition,
        string $from,
        string $to
    ): bool {
        return $subject->amount > 0;
    }

    #[Guard('ship')]
    public function hasShippingAddress(
        object|array $subject,
        string $transition,
        string $from,
        string $to
    ): bool {
        return $subject->shipping_address !== null;
    }
}

$machine = StateMachine::create(
    definition: OrderStateDefinition::class,
    workflow: OrderWorkflow::class,
);

$order->amount = 0;
$machine->can($order, 'pay');   // false
$machine->apply($order, 'pay'); // throws GuardRejectedException

$order->amount = 100;
$machine->can($order, 'pay');   // true
$machine->apply($order, 'pay'); // works!

#[Guard('pay')]
public function hasValidAmount($subject, ...): bool {
    return $subject->amount > 0;
}

#[Guard('pay')]
public function hasCustomer($subject, ...): bool {
    return $subject->customer_id !== null;
}

// Both must return true for 'pay' to be allowed



use Stateflow\Engine\Attribute\Guard;
use Stateflow\Engine\Attribute\Before;
use Stateflow\Engine\Attribute\OnExit;
use Stateflow\Engine\Attribute\OnEnter;
use Stateflow\Engine\Attribute\After;

final class OrderWorkflow
{
    #[Guard('pay')]
    public function hasValidAmount($subject, $transition, $from, $to): bool
    {
        return $subject->amount > 0;
    }

    #[Before('pay')]
    public function logPaymentAttempt($subject, $transition, $from, $to): void
    {
        Log::info("Attempting payment for order {$subject->id}");
    }

    #[OnExit('pending')]
    public function onLeavePending($subject, $transition, $from, $to): void
    {
        Log::info("Order {$subject->id} is no longer pending");
    }

    #[OnEnter('paid')]
    public function sendConfirmationEmail($subject, $transition, $from, $to): void
    {
        Mail::send(new OrderConfirmation($subject));
    }

    #[After('pay')]
    public function notifyWarehouse($subject, $transition, $from, $to): void
    {
        Warehouse::notify($subject);
    }
}

function(
    object|array $subject,  // The subject being transitioned
    string $transition,     // The transition name ('pay')
    string $from,           // The previous state ('pending')
    string $to              // The new state ('paid')
): void

$machine->onTransition(function (TransitionEvent $event) {
    // Log to database
    AuditLog::create([
        'entity_type' => get_class($event->subject),
        'entity_id' => $event->subject->id,
        'transition' => $event->transition,
        'from' => $event->from,
        'to' => $event->to,
        'occurred_at' => $event->occurredAt,
        'user_id' => auth()->id(),
    ]);
});

$event->subject;     // The subject that transitioned
$event->transition;  // Transition name ('pay')
$event->from;        // Previous state ('pending')
$event->to;          // New state ('paid')
$event->occurredAt;  // DateTimeImmutable

$machine
    ->onTransition(function ($event) {
        // Log to database
    })
    ->onTransition(function ($event) {
        // Send to analytics
    })
    ->onTransition(function ($event) {
        // Notify webhooks
    });

use Stateflow\Engine\Accessor\CallbackAccessor;

$accessor = new CallbackAccessor(
    getter: fn($subject, $field) => $subject->getCurrentState(),
    setter: fn(&$subject, $field, $state) => $subject->changeStateTo($state),
);

$machine = StateMachine::create(
    definition: OrderStateDefinition::class,
    accessor: $accessor,
);

use Stateflow\Engine\Accessor\StateAccessorInterface;

final class MyCustomAccessor implements StateAccessorInterface
{
    public function getState(object|array $subject, string $field): ?string
    {
        // Your logic
    }

    public function setState(object|array &$subject, string $field, string $state): void
    {
        // Your logic
    }
}

echo $machine->toMermaid();

// app/StateMachines/OrderStateDefinition.php
final class OrderStateDefinition
{
    public const STATES = ['pending', 'paid', 'shipped', 'cancelled'];
    public const TRANSITIONS = [
        'pay' => ['from' => ['pending'], 'to' => 'paid'],
        'ship' => ['from' => ['paid'], 'to' => 'shipped'],
        'cancel' => ['from' => ['pending', 'paid'], 'to' => 'cancelled'],
    ];
}

// app/Models/Order.php
class Order extends Model
{
    public function pay(): void
    {
        $machine = StateMachine::create(OrderStateDefinition::class);
        $machine->apply($this, 'pay');
        $this->save();
    }
}

// Usage
$order = Order::find(1);
$order->pay();

// AppServiceProvider
$this->app->singleton('order.machine', function () {
    return StateMachine::create(
        definition: OrderStateDefinition::class,
        workflow: OrderWorkflow::class,
        initialState: 'pending',
    );
});

// Usage
app('order.machine')->apply($order, 'pay');

// src/Entity/Order.php
#[ORM\Entity]
class Order
{
    #[ORM\Column]
    private string $status = 'pending';

    public function getStatus(): string
    {
        return $this->status;
    }

    public function setStatus(string $status): void
    {
        $this->status = $status;
    }
}

// Usage
$machine = StateMachine::create(OrderStateDefinition::class);
$machine->apply($order, 'pay');
$entityManager->flush();

use Stateflow\Engine\Exception\InvalidTransitionException;
use Stateflow\Engine\Exception\InvalidStateException;
use Stateflow\Engine\Exception\GuardRejectedException;
use Stateflow\Engine\Exception\ValidationException;
use Stateflow\Engine\Exception\StateAccessException;

try {
    $machine->apply($order, 'pay');
} catch (InvalidTransitionException $e) {
    // Transition doesn't exist or not allowed from current state
} catch (GuardRejectedException $e) {
    // A guard blocked the transition
    echo "Blocked by: " . $e->guardMethod;
} catch (InvalidStateException $e) {
    // Subject has invalid state
}

// Catch all StateFlow exceptions
use Stateflow\Engine\Exception\StateflowExceptionInterface;

try {
    $machine->apply($order, 'pay');
} catch (StateflowExceptionInterface $e) {
    // Any StateFlow error
}

public function test_order_can_be_paid(): void
{
    $machine = StateMachine::create(OrderStateDefinition::class);
    $order = ['status' => 'pending', 'amount' => 100];

    $machine->apply($order, 'pay');

    $this->assertSame('paid', $order['status']);
}

public function test_shipped_order_cannot_be_cancelled(): void
{
    $machine = StateMachine::create(OrderStateDefinition::class);
    $order = ['status' => 'shipped'];

    $this->assertFalse($machine->can($order, 'cancel'));
}

public function test_guard_blocks_zero_amount(): void
{
    $machine = StateMachine::create(
        definition: OrderStateDefinition::class,
        workflow: OrderWorkflow::class,
    );
    $order = ['status' => 'pending', 'amount' => 0];

    $this->expectException(GuardRejectedException::class);

    $machine->apply($order, 'pay');
}



use Stateflow\Engine\StateMachine;
use Stateflow\Engine\Attribute\Guard;
use Stateflow\Engine\Attribute\OnEnter;
use Stateflow\Engine\Event\TransitionEvent;

// 1. Definition (pure data)
final class OrderStateDefinition
{
    public const STATES = ['pending', 'paid', 'shipped', 'cancelled'];

    public const TRANSITIONS = [
        'pay' => ['from' => ['pending'], 'to' => 'paid'],
        'ship' => ['from' => ['paid'], 'to' => 'shipped'],
        'cancel' => ['from' => ['pending', 'paid'], 'to' => 'cancelled'],
    ];
}

// 2. Workflow (behavior)
final class OrderWorkflow
{
    #[Guard('pay')]
    public function hasValidAmount($subject, $transition, $from, $to): bool
    {
        return $subject->amount > 0;
    }

    #[Guard('ship')]
    public function hasShippingAddress($subject, $transition, $from, $to): bool
    {
        return $subject->shipping_address !== null;
    }

    #[OnEnter('paid')]
    public function sendConfirmation($subject, $transition, $from, $to): void
    {
        Mail::send(new OrderConfirmation($subject));
    }

    #[OnEnter('shipped')]
    public function sendTrackingInfo($subject, $transition, $from, $to): void
    {
        Mail::send(new ShippingNotification($subject));
    }

    #[OnEnter('cancelled')]
    public function processRefund($subject, $transition, $from, $to): void
    {
        if ($from === 'paid') {
            PaymentGateway::refund($subject);
        }
    }
}

// 3. Usage
$machine = StateMachine::create(
    definition: OrderStateDefinition::class,
    workflow: OrderWorkflow::class,
    initialState: 'pending',
);

// Audit trail
$machine->onTransition(function (TransitionEvent $event) {
    AuditLog::create([
        'order_id' => $event->subject->id,
        'transition' => $event->transition,
        'from' => $event->from,
        'to' => $event->to,
        'occurred_at' => $event->occurredAt,
    ]);
});

// Initialize new order
$order = new Order();
$machine->initialize($order); // status = 'pending'

// Process order
$order->amount = 99.99;
$order->shipping_address = '123 Main St';

$machine->apply($order, 'pay');  // Sends confirmation email
$machine->apply($order, 'ship'); // Sends tracking info

// Check what's possible
$machine->getAvailableTransitions($order); // [] - shipped is final

// Visualize
echo $machine->toMermaid();