adachsoft/workflow

Lightweight, extensible PHP workflow engine with plugin-based steps, context isolation, debugging and nested workflow support

Maintainers

Package info

gitlab.com/a.adach/Workflow

Issues

pkg:composer/adachsoft/workflow

Statistics

Installs: 17

Dependents: 1

Suggesters: 0

Stars: 0

v3.0.1 2026-05-21 05:42 UTC

README

Simple, plugin-based workflow engine for PHP with branch-first step contracts and built-in payload schema validation.

Requirements

  • PHP 8.2+
  • adachsoft/collection ^3.0

Installation

composer require adachsoft/workflow

Overview

The library provides two engines built around three central contracts:

  • WorkflowDefinitionRepositoryInterface
  • WorkflowRunRepositoryInterface
  • StepRegistryInterface

Available engines:

  • WorkflowEngineInterface for synchronous workflow execution
  • DebugEngineInterface for step-by-step workflow debugging

You can build ready-to-use in-memory engines with:

  • WorkflowEngineBuilder
  • DebugEngineBuilder

Main Concepts

Step (branch-first)

A workflow step implements StepInterface and exposes metadata through #[StepType]:

  • inputSchema - expected step input
  • configSchema - expected step config
  • branches - declared execution branches and output schemas

A step returns StepResultDto::branch('<branch_name>', $output).

The engine resolves the next workflow step from current step config[<branch_name>] for non-terminal branches.

Workflow definition

A workflow definition is represented by WorkflowDefinitionVo and stores named workflow steps as PayloadVo entries.

The first defined key is treated as the entry step.

Workflow run

A workflow run is represented by WorkflowRunVo and stores:

  • current step
  • input
  • output
  • finished state
  • per-run context (WorkflowRunContextVo)

Payload

PayloadVo is an immutable value object used for step input, step config, and step output.

Schema Validation

WorkflowEngine validates payloads in three phases:

  • Input phase (before execution) - permissive mode (extra keys allowed)
  • Config phase (before execution) - strict mode
  • Output phase (after execution, per selected branch) - strict mode

Validation errors raise SchemaValidationException.

Branch contract violations can raise:

  • UnknownBranchException
  • BranchTargetMissingException

Basic Usage

use AdachSoft\Workflow\Attribute\StepType;
use AdachSoft\Workflow\Builder\WorkflowEngineBuilder;
use AdachSoft\Workflow\Contracts\StepInterface;
use AdachSoft\Workflow\Engine\PayloadVo;
use AdachSoft\Workflow\Engine\StepContextDto;
use AdachSoft\Workflow\Engine\StepResultDto;
use AdachSoft\Workflow\Engine\WorkflowDefinitionVo;
use AdachSoft\Workflow\Registry\InMemoryStepRegistry;
use AdachSoft\Workflow\Repository\InMemoryWorkflowDefinitionRepository;
use AdachSoft\Workflow\Repository\InMemoryWorkflowRunRepository;

#[StepType(
    type: 'append_message',
    description: 'Appends configured message',
    inputSchema: [
        'messages' => ['type' => 'array', 'required' => true, 'description' => 'Current messages'],
    ],
    configSchema: [
        'message' => ['type' => 'string', 'required' => true, 'description' => 'Message to append'],
        'on_next' => ['type' => 'string', 'required' => true, 'description' => 'Next step name'],
    ],
    branches: [
        'on_next' => [
            'description' => 'Continue workflow',
            'terminal' => false,
            'outputSchema' => [
                'messages' => ['type' => 'array', 'required' => true, 'description' => 'Updated messages'],
            ],
        ],
    ],
)]
final class AppendMessageStep implements StepInterface
{
    public function execute(StepContextDto $stepContextDto): StepResultDto
    {
        $messages = $stepContextDto->input->get('messages');
        $message = $stepContextDto->config->get('message');

        if (!is_array($messages) || !is_string($message)) {
            return StepResultDto::branch('on_next', new PayloadVo(['messages' => []]));
        }

        $messages[] = $message;

        return StepResultDto::branch('on_next', new PayloadVo([
            'messages' => $messages,
        ]));
    }
}

#[StepType(
    type: 'finish',
    description: 'Finishes workflow',
    inputSchema: [
        'messages' => ['type' => 'array', 'required' => true, 'description' => 'Final messages'],
    ],
    branches: [
        'end' => [
            'description' => 'Finish run',
            'terminal' => true,
            'outputSchema' => [
                'status' => ['type' => 'string', 'required' => true, 'description' => 'Final status'],
                'messages' => ['type' => 'array', 'required' => true, 'description' => 'Final messages'],
            ],
        ],
    ],
)]
final class FinishStep implements StepInterface
{
    public function execute(StepContextDto $stepContextDto): StepResultDto
    {
        return StepResultDto::branch('end', new PayloadVo([
            'status' => 'done',
            'messages' => $stepContextDto->input->get('messages'),
        ]));
    }
}

$stepRegistry = new InMemoryStepRegistry();
$stepRegistry->register(AppendMessageStep::class);
$stepRegistry->register(FinishStep::class);

$definitionRepository = new InMemoryWorkflowDefinitionRepository();
$runRepository = new InMemoryWorkflowRunRepository();

$workflowEngine = WorkflowEngineBuilder::create()
    ->withStepRegistry($stepRegistry)
    ->withWorkflowDefinitionRepository($definitionRepository)
    ->withWorkflowRunRepository($runRepository)
    ->build();

$definitionRepository->save(new WorkflowDefinitionVo('example', [
    'append' => new PayloadVo([
        'type' => 'append_message',
        'config' => new PayloadVo([
            'message' => 'processed',
            'on_next' => 'finish',
        ]),
    ]),
    'finish' => new PayloadVo([
        'type' => 'finish',
        'config' => PayloadVo::empty(),
    ]),
]));

$runId = $workflowEngine->start('example', new PayloadVo([
    'messages' => ['created'],
]));

$run = $runRepository->get($runId);

Builders

WorkflowEngineBuilder and DebugEngineBuilder provide two usage modes:

  • build a default in-memory engine
  • build an engine with custom repositories and a custom step registry

Example:

use AdachSoft\Workflow\Builder\DebugEngineBuilder;

$debugEngine = DebugEngineBuilder::create()->build();

Built-in Steps

The package currently provides these built-in steps in src/Steps:

  • RunWorkflowStep - starts another workflow and forwards workflow output
  • SleepStep - pauses execution for configured seconds

Builders can register built-in steps for you:

$workflowEngine = WorkflowEngineBuilder::create()
    ->withStandardSteps()
    ->build();

Debug Engine

DebugEngineInterface supports:

  • starting a workflow in paused mode
  • starting from a selected step
  • executing one step at a time
  • pausing and resuming a run
  • reading the current run state

Step Metadata

The registry can expose all registered step definitions via StepRegistryInterface::getAllDefinitions().

This returns StepDefinitionCollection and can be serialized using collection export methods.

Design Notes

  • Engines depend on contracts for repositories and the step registry.
  • Workflow execution is synchronous.
  • Debug engine executes one step at a time.
  • Public API uses immutable value objects and collections.
  • Step metadata and branch contracts are centralized in #[StepType].
  • Input/config/output validation is integrated into runtime execution.

Development

Available Composer scripts:

composer test
composer phpstan
composer cs-fix
composer cs-check