chatflowphp / automata
Framework-agnostic state machine runtime for persistent step-by-step flows in PHP: chat dialogs, wizards, workflows.
2.0.0
2026-09-12 14:38 UTC
Requires
- php: >=8.1
- psr/clock: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A framework-agnostic state machine runtime for flows that advance one input at a time and must survive between requests: chat bots, multi-step forms, approval workflows.
$session = Session::resume($store, 'chat:' . $chatId, fn () => $this->buildMachine($replies), AskNameState::ID); if (!$session->isNew()) { $session->tick(new IncomingMessage($text)); } $session->persist(); return $replies->all(); // ['Nice to meet you, Alice. How old are you?']
What you get
- States as classes. Extend
AbstractState, declare the input type, implementhandle().onEnter()can reply or chain into the next state. - Declared transitions. A
TransitionTablewith guards rejects illegal jumps and renders as a Mermaid diagram. Or allow everything and route dynamically. - Atomic ticks. Context, current state, and state data roll back on any exception. Listeners run after commit and never see a half-applied tick.
- Typed messaging. Commands and events are plain objects; subscribe by class, including interfaces for wildcards.
- Versioned snapshots. JSON with a schema version, migrations, tick counter, and timestamp.
SessionplusSnapshotStoreInterfacegive you the request cycle in three lines. - Middleware around the whole tick, for transactions, persistence, or tracing.
What it is not
- Not a statechart engine: no parallel regions, no history states.
- Not a message transport: the bus is synchronous and in-process.
- Not a persistence layer: bring your own
SnapshotStoreInterface.
Install
composer require chatflowphp/automata
PHP 8.1 or newer. The only runtime dependency is psr/clock.
A state
/** @extends AbstractState<IncomingMessage> */ final class AskAgeState extends AbstractState { public const ID = 'survey.ask_age'; protected const INPUT = IncomingMessage::class; public function getId(): string { return self::ID; } public function onEnter(ContextInterface $context): CycleResponse { return CycleResponse::fromEvent(new BotReply(sprintf('Nice to meet you, %s. How old are you?', $context->getString('name')))); } protected function handle(InputInterface $input, ContextInterface $context): CycleResponse { if (!ctype_digit($input->text)) { return CycleResponse::fromEvent(new BotReply('Please enter a number.')); } $context->set('age', (int) $input->text); return CycleResponse::transitionTo(ConfirmState::ID); } }
A machine
$machine = new StateMachine(new ArrayContext(), transitions: TransitionTable::define([ AskNameState::ID => [AskAgeState::ID], AskAgeState::ID => [ConfirmState::ID => fn (ContextInterface $c): bool => $c->getInt('age') > 0], ConfirmState::ID => [DoneState::ID, AskNameState::ID], ])); $machine->registerStates(new AskNameState(), new AskAgeState(), new ConfirmState(), new DoneState()); $machine->subscribe(BotReply::class, $replies); $machine->start(AskNameState::ID); // "Hi! What is your name?" $result = $machine->tick(new IncomingMessage('Alice')); $result->toStateId; // survey.ask_age $result->messagesOf(BotReply::class); // the question asked by the new state
Documentation
Start at the documentation hub:
- Getting Started, the survey bot step by step
- State Machine, States, Transitions
- Messaging, Context, Middleware
- Snapshots, Sessions, Testing
- Upgrade from 1.x
Examples
| Example | Shows |
|---|---|
| survey-bot | The canonical chat flow: sessions, snapshot store, guards, replies from onEnter() |
| simple-workflow | The smallest machine: two states, middleware, one event, snapshot round trip |
| traffic-light | Declared transition graph, shared state base class, injected clock, resumed execution |
php examples/survey-bot/run.php
Development
composer check # validate, code style, phpstan, tests
Versioning
2.0 is a rewrite of 1.x with a new API. See CHANGELOG.md and the upgrade guide. Snapshots written by 1.x can be migrated.
License
MIT, see LICENSE.