Skip to content

Helvetitec/flowengine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

56 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FlowEngine

A simple, flexible state machine engine for Laravel to handle dynamic flows like chats, onboarding, workflows, and more.

πŸš€ Concept

FlowEngine allows you to define state-driven flows where each subject (e.g. a chat, user, or process) moves through states based on input.

Typical flow:

  1. Receive input
  2. Process current state
  3. Transition to next state
  4. Persist state + context
  5. Stop execution
  6. Resume later (via new input or cooldown)

Setup

  1. Publish the migrations:
php artisan vendor:publish --tag="helvetitec.flowengine.migrations"
  1. Run migrations:
php artisan migrate

🧱 Core Components

FlowEngine

The base class that handles execution:

abstract class FlowEngine
{
    abstract protected function doRun(mixed $input): void;

    final public function run(FlowSubject $subject, mixed $input): void;

    final protected function subject(): FlowSubject;
    
    final protected function cooldown(?Carbon $until): static;

    final protected function transition(string $nextState): static;

    final protected function set(string $key, mixed $value): static;

    final protected function get(string $key, mixed $default = null): mixed;

    final protected function pull(string $key, mixed $default = null, bool $persist = false): mixed;

    final protected function delete(string $key): static;

    final protected function stop(bool $persist = true): never;

    final protected function transitionAndStop(string $nextState): never;

    final protected function deactivate(): never;
}

FlowSubject

Any model that participates in a flow must implement:

interface FlowSubject
{
    public function getActive(): bool;
    public function setActive(bool $active): void;

    public function getStateKey(): string;
    public function setStateKey(string $state): void;

    public function getContext(): array;
    public function setContext(?array $context): void;

    public function getCooldown(): ?Carbon;
    public function setCooldown(?Carbon $until): void;

    public function persist(): void;
}

FlowRun

class FlowRun extends Model implements FlowSubject
{
    public function subject();

    public function getActive(): bool;

    public function setActive(bool $active): void;
    
    public function getStateKey(): string;

    public function setStateKey(string $state): void;

    public function getContext(): array;

    public function setContext(?array $context): void;

    public function getCooldown(): ?Carbon;

    public function setCooldown(?Carbon $until): void;

    public function persist(): void;

    public function resolveFlow(): FlowEngine;

    public function runFlow(mixed $input = null, bool $force = false): void;

    public function mergeContext(array $data): static;

    public static function clear(string $flowClass, ?Carbon $clearOlderThan = null, ?string $flowType = null, ?string $flowId = null): int;
}

⚑ Example Implementation

1. FlowRuns

FlowRuns allow multiple FlowEngines running at the same time. The default state for every run is always 'start'.

//Add to your Subject (e.g Chat)
use HasFlowRuns;

2. Flow

class ChatFlow extends FlowEngine
{
    protected function doRun(mixed $input): void
    {
        $state = $this->subject()->getStateKey();

        if(!($this->subject() instanceof Chat)){
            throw new LogicException("Subject is not instance of Chat!");
        }

        match ($state) {
            'start' => $this->start(),
            'waiting' => $this->handleAnswer($input),
            default => $this->start(),
        };
    }

    private function start(): void
    {
        ChatService::send($this->subject(), "Choose 1 or 2");

        $this->transition('waiting')
             ->set('options', [1,2]) //Sets the context for the flow
             ->stop();
    }

    private function handleAnswer($input): void
    {
        $options = $this->get('options');
        if(!in_array($input, $options)){
            ChatService::send($this->subject(), "Invalid input");
            $this->stop();
            return;
        }
        ChatService::send($this->subject(), "You chose: {$input}");

        $this->transition('done')
             ->delete('options')
             ->cooldown(now()->addMinutes(5))
             ->stop();
    }
}

3. Triggering the Flow

$chat->runFlow(ChatFlow::class, $message, $force);
//With merged context
$chat->startFlow(ChatFlow::class)->mergeContext(['some_context_to_start' => 'Hello World'])->runFlow("input");
//Update the context for all FlowRuns of the model at once.
$chat->broadcastContext(['context_for_all_runs' => true]);
//Returns the object related to the flow. In this case it would be $chat as well as it is the owner, but its powerful inside the FlowEngine as you can call subject()->getOwner().
$chat->startFlow(ChatFlow::class)->subject()->getOwner();
//Clears all flowruns older than a specific date with the selected flowclass
FlowRun::clear(ChatFlow::class, now()->subMonth());
//Clears all flowruns from a specific model
FlowRun::clear(ChatFlow::class, now()->subMonth(), Chat::class, 1);
//Clears all flowruns older than a specific date. Prefer to use the more specific ones!
FlowRun::clearAll(now()->subMonth());

You typically call this from:

  • Controllers
  • Jobs
  • Event listeners
  • Webhooks

πŸ” Flow Lifecycle

Input β†’ run() β†’ doRun()
        ↓
   state logic
        ↓
 transition()
 set()
 cooldown()
        ↓
     stop()
        ↓
   persist()

🧩 Helper Methods

Transition state

Handles the transition between states.

$this->transition('next_state');

Store data in context

Stores and loads data from the context.

$this->set('key', 'value');

$value = $this->get('key');

Pull data from context and delete

$value = $this->pull('key');

Delete data from context

$this->delete('key');

Clear all data from context

$this->clear();

Cooldown

Adds a cooldown between this run and the next run.

$this->cooldown(now()->addMinutes(10));

Stop execution

Stops the execution of the current flow.

$this->stop(); //persists

or

$this->stop(persist: false); //does not persist

Transition and Stop

Sets the next state and stops.

$this->transitionAndStop('next_state');

Deactivate

Deactivates the flow and stops it.

$this->deactivate();

Pause flow

$this->pause(now()->addMinutes(5));

Reset Flow

$this->reset(now()->addHour());

⚠️ Important Rules

1. Always use run()

// βœ… Correct
app(MyFlow::class)->run($subject, $input);

// ❌ Wrong
$flow->doRun($input);

2. Always stop after sending output

$this->transition('next')
     ->stop();

3. Do not persist manually

Persistence is handled automatically by the engine.

4. Use state keys as strings

'start'
'waiting_for_input'
'completed'

5. Hints

Exceptions

The FlowEngine will throw a FlowEngineException if you call FlowEngine->run(). This exception has the following additional context for easier debugging: flow_engine_class Returns the class of the FlowEngine like ChatFlowExample.

flow_engine_context Returns the current context of the FlowSubject.

flow_engine_state Returns the current state of the FlowSubject.

input Returns the latest input of the run.

Enable/Disable flows

You can fully disable flows by setting the setActive/getActive methods to a custom field or use setActive in FlowRuns.

protected $fillable = [
    'flow_active'
];

protected $casts = [
    'flow_active' => 'boolean'
];

public function setActive(bool $active): void
{
    $this->flow_active = $active;
}

public function getActive(): bool
{
    return $this->flow_active;
}

6. πŸ“„ AI Usage

AI was used to create this readme file and for smaller parts of the code to make it cleaner.

About

Flow Engine for Laravel

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages