adachsoft / workflow
Framework-agnostic workflow Public API with plugin system for external steps (PublicApi\Plugin).
Requires
- php: ^8.2
- adachsoft/collection: ^2.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.87
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.3
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
, optionalnextStepId
,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
+ metadataTagCollection
).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 DomainStepRegistryPortInterface
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 incomposer.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 forrunId
,workflowDefinitionId
,stepId
and PublicApi collections (ApiInputDataCollection
,ApiSharedStateCollection
).DomainStepAdapter
maps Domain context to Plugin context and back:status
: 1:1 mapping between PluginStepResultStatusEnum
and Domain enum.nextStepId
: string ↔ DomainStepId
.output
:ApiOutputDataCollection
↔ DomainOutputDataCollection
.
Error Handling & Troubleshooting
- Type not registered:
StepRegistryPortAdapter::getByType('unknown')
throwsStepNotFoundInRegistryException
.
- Lookup by ID not supported by Plugin API:
StepRegistryPortAdapter::getById($id)
always throwsStepNotFoundInRegistryException
.
- Invalid provider in composer.json:
ComposerExtraStepProviderDiscovery
throwsInvalidArgumentException
if the class does not exist or does not implementStepProviderInterface
.
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.