adachsoft/workflow

Framework-agnostic workflow Public API with plugin system for external steps (PublicApi\Plugin).

v0.0.1 2025-09-29 07:51 UTC

This package is not auto-updated.

Last update: 2025-09-30 03:36:56 UTC


README

This repository exposes a stable, framework-agnostic Public API that allows external libraries (plugins) to provide workflow steps without depending on the Domain layer. The application hosts can register steps either manually or via automatic discovery using composer.json extra configuration.

  • Decoupled from Domain: plugins implement only contracts from AdachSoft\Workflow\PublicApi\Plugin.
  • Two registration modes:
    • Manual in the host application.
    • Automatic via discovery of providers listed in composer.json.
  • Domain engine consumes steps through an adapter in Application\PluginBridge.

Requirements

  • PHP >= 8.2
  • adachsoft/collection >= 2.1.0

Concepts at a Glance

  • StepInterface: the plugin step to be executed by the engine.
  • StepContextDto: immutable input context for step execution (runId, workflowDefinitionId, stepId, input, sharedState).
  • StepResultDto: the result of step execution (status, optional nextStepId, output).
  • StepResultStatusEnum: possible execution statuses: SUCCESS, RETRY, FAILURE, SKIPPED.
  • StepFactoryInterface: factory for creating step instances.
  • StepProviderInterface: provides multiple step registrations shipped by a plugin.
  • StepRegistrationDto: a single registration entry (type + factory + metadata TagCollection).
  • Tag / TagCollection: simple metadata attached to registration entries.

Host-side bridge and discovery:

  • Application\PluginBridge\StepRegistrationService: stores registrations and exposes factories by type.
  • Application\PluginBridge\StepRegistryPortAdapter: implements Domain StepRegistryPortInterface on top of Plugin API registrations.
  • Application\PluginBridge\DomainStepAdapter: runs a Plugin step inside the Domain engine by mapping DTOs and enums.
  • Infrastructure\PluginDiscovery\ComposerExtraStepProviderDiscovery: finds providers declared in composer.json.

Quick Start — Plugin Author

1) Implement a Step

<?php
declare(strict_types=1);

namespace Vendor\Plugin\Step;

use AdachSoft\Workflow\PublicApi\Plugin\Dto\StepContextDto;
use AdachSoft\Workflow\PublicApi\Plugin\Dto\StepResultDto;
use AdachSoft\Workflow\PublicApi\Plugin\Enum\StepResultStatusEnum;
use AdachSoft\Workflow\PublicApi\Plugin\Step\StepInterface;

final class MyStep implements StepInterface
{
    public function execute(StepContextDto $context): StepResultDto
    {
        // Read input
        $input = iterator_to_array($context->input);

        // Do work...

        // Return success with optional output
        return new StepResultDto(
            status: StepResultStatusEnum::SUCCESS,
            nextStepId: null,
            output: new \AdachSoft\Workflow\PublicApi\Collection\ApiOutputDataCollection([
                'processed' => true,
            ])
        );
    }
}

2) Provide a Factory

<?php
declare(strict_types=1);

namespace Vendor\Plugin\Step;

use AdachSoft\Workflow\PublicApi\Plugin\Factory\StepFactoryInterface;
use AdachSoft\Workflow\PublicApi\Plugin\Step\StepInterface;

final class MyStepFactory implements StepFactoryInterface
{
    public function create(): StepInterface
    {
        return new MyStep();
    }
}

3) Provide a Provider (optional but recommended for discovery)

<?php
declare(strict_types=1);

namespace Vendor\Plugin\Provider;

use AdachSoft\Workflow\PublicApi\Plugin\Collection\Tag;
use AdachSoft\Workflow\PublicApi\Plugin\Collection\TagCollection;
use AdachSoft\Workflow\PublicApi\Plugin\Provider\StepProviderInterface;
use AdachSoft\Workflow\PublicApi\Plugin\Registration\StepRegistrationDto;
use Vendor\Plugin\Step\MyStepFactory;

final class ExampleProvider implements StepProviderInterface
{
    public function provide(): iterable
    {
        yield new StepRegistrationDto(
            type: 'my_step',
            factory: new MyStepFactory(),
            metadata: new TagCollection([
                new Tag('category', 'demo'),
                new Tag('version', '1.0.0'),
            ])
        );
    }
}

4) Configure composer.json in your plugin repository

{
  "name": "vendor/plugin-workflow-steps",
  "type": "library",
  "autoload": {
    "psr-4": {"Vendor\\Plugin\\": "src/"}
  },
  "extra": {
    "adachsoft-workflow": {
      "step-providers": [
        "Vendor\\Plugin\\Provider\\ExampleProvider"
      ]
    }
  }
}

Quick Start — Host Application

1) Manual registration (no discovery)

<?php
declare(strict_types=1);

use AdachSoft\Workflow\Application\PluginBridge\StepRegistrationService;
use AdachSoft\Workflow\Application\PluginBridge\StepRegistryPortAdapter;
use AdachSoft\Workflow\Infrastructure\InMemory\Step\InMemoryDomainStepRegistry; // example of previous usage (to be replaced)
use AdachSoft\Workflow\Domain\Workflow\Engine\SimpleExecutionEngine;
use Vendor\Plugin\Step\MyStepFactory;

$registration = new StepRegistrationService();
$registration->registerType('my_step', new MyStepFactory());

$registryPort = new StepRegistryPortAdapter($registration);

$engine = new SimpleExecutionEngine(
    definitionRepository: $definitionRepository,
    runRepository: $runRepository,
    runDataRepository: $runDataRepository,
    stepRegistry: $registryPort,        // ← use Plugin-based registry
    scheduler: $scheduler,
    eventPublisher: $eventPublisher,
);

2) Automatic registration via composer.json discovery

<?php
declare(strict_types=1);

use AdachSoft\Workflow\Application\PluginBridge\StepRegistrationService;
use AdachSoft\Workflow\Application\PluginBridge\StepRegistryPortAdapter;
use AdachSoft\Workflow\Infrastructure\PluginDiscovery\ComposerExtraStepProviderDiscovery;
use AdachSoft\Workflow\Domain\Workflow\Engine\SimpleExecutionEngine;

$registration = new StepRegistrationService();
$discovery = new ComposerExtraStepProviderDiscovery(); // reads composer.json by default

foreach ($discovery->discover() as $provider) {
    $registration->registerProvider($provider);
}

$registryPort = new StepRegistryPortAdapter($registration);

$engine = new SimpleExecutionEngine(
    definitionRepository: $definitionRepository,
    runRepository: $runRepository,
    runDataRepository: $runDataRepository,
    stepRegistry: $registryPort,
    scheduler: $scheduler,
    eventPublisher: $eventPublisher,
);

3) Mixed: manual + discovery

$registration->registerType('fallback_step', new Vendor\Plugin\Step\FallbackStepFactory());
foreach ($discovery->discover() as $provider) {
    $registration->registerProvider($provider);
}

Data Mapping (Plugin ↔ Domain)

  • StepContextDto (Plugin) uses strings for runId, workflowDefinitionId, stepId and PublicApi collections (ApiInputDataCollection, ApiSharedStateCollection).
  • DomainStepAdapter maps Domain context to Plugin context and back:
    • status: 1:1 mapping between Plugin StepResultStatusEnum and Domain enum.
    • nextStepId: string ↔ Domain StepId.
    • output: ApiOutputDataCollection ↔ Domain OutputDataCollection.

Error Handling & Troubleshooting

  • Type not registered:
    • StepRegistryPortAdapter::getByType('unknown') throws StepNotFoundInRegistryException.
  • Lookup by ID not supported by Plugin API:
    • StepRegistryPortAdapter::getById($id) always throws StepNotFoundInRegistryException.
  • Invalid provider in composer.json:
    • ComposerExtraStepProviderDiscovery throws InvalidArgumentException if the class does not exist or does not implement StepProviderInterface.

Full Minimal Examples

1) Minimal Plugin (Step + Factory + Provider)

// src/Step/HelloStep.php
final class HelloStep implements StepInterface { /* ... */ }

// src/Step/HelloStepFactory.php
final class HelloStepFactory implements StepFactoryInterface { /* ... */ }

// src/Provider/HelloProvider.php
final class HelloProvider implements StepProviderInterface {
    public function provide(): iterable {
        yield new StepRegistrationDto(
            type: 'hello',
            factory: new HelloStepFactory(),
            metadata: new TagCollection([ new Tag('category', 'sample') ])
        );
    }
}
// composer.json (plugin repository)
{
  "extra": {
    "adachsoft-workflow": {
      "step-providers": [
        "Vendor\\Plugin\\Provider\\HelloProvider"
      ]
    }
  }
}

2) Minimal Host Wiring (manual + discovery)

$registration = new StepRegistrationService();
$registration->registerType('hello', new Vendor\Plugin\Step\HelloStepFactory());

$discovery = new ComposerExtraStepProviderDiscovery();
foreach ($discovery->discover() as $provider) {
    $registration->registerProvider($provider);
}

$stepRegistry = new StepRegistryPortAdapter($registration);
$engine = new SimpleExecutionEngine(/* inject repositories, $stepRegistry, scheduler, eventPublisher */);

Notes

  • This library is framework-agnostic. Compose the graph of dependencies manually (no container required).
  • Collections always use adachsoft/collection.
  • Keep plugin steps cohesive and side-effect free as much as possible.

If you have questions or need more examples (advanced routing, retries, metadata usage), please open an issue or contact the maintainers.