lemric / psr-wf
Installs: 2
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/lemric/psr-wf
Requires
- php: ^8.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.88
This package is auto-updated.
Last update: 2025-10-12 16:00:07 UTC
README
Status: Draft
Target PHP: 8.4+
Dependencies: PSR-3 (Logger), PSR-11 (Container), PSR-14 (Events), PSR-6/16 (Cache – optional)
Authors: Lemric Group
Purpose: Define interoperable contracts and execution semantics for a finite state machine workflow (single active state) with conditions, validators, post-functions, triggers, and schemes, portable across frameworks (Symfony/Laravel/custom).
1. Motivation & Goals
Modern PHP applications often require configurable business processes: status transitions, gatekeeping rules, validations, and side-effects. Existing solutions are framework-specific and incompatible. PSR‑WF defines standardized interfaces and semantics to:
- express a single-marking finite state machine (FSM) for domain subjects;
- provide extensible rule chains: Conditions, Validators, PostFunctions, and Triggers;
- map workflow definitions to contexts via schemes;
- integrate with PSR‑14 events, PSR‑11 containers, and PSR‑3 logging;
- remain agnostic of persistence, configuration format, and UI.
Non-goals: BPMN modeling, multi-marking/parallel tokens, job orchestration, storage and transport formats.
2. Terminology
- Subject – domain object governed by a workflow (e.g., Issue, Order).
- Status – single active state of a Subject (e.g.,
open
,in_progress
,done
). - Transition – directed edge
from[] -> to
guarded by rules and actions. - Condition – decides if a Transition is visible/allowed for the Actor+Context.
- Validator – asserts runtime data correctness at execution time (may raise error).
- PostFunction – performs side-effects after a successful status change.
- Trigger – external or time-based cause that attempts a Transition.
- Workflow – named graph of Statuses and Transitions with an initial status.
- Scheme – resolver mapping Contexts (e.g., project/type) to Workflow keys.
- Context – metadata describing execution environment (project, tenant, locale).
- Actor – user/system identity attempting or executing a Transition.
- Engine – component evaluating rules and executing Transitions atomically.
- MarkingStore – reads/writes the Subject’s current Status.
3. Design Principles
- Interoperability first – portable contracts; no framework assumptions.
- Determinism – defined evaluation order and transaction semantics.
- Extensibility – Strategy + Chain-of-Responsibility for all rule types.
- Separation of concerns – definition vs. execution vs. persistence vs. UI.
- Safety – explicit error classes, auditability hooks, least privilege bias.
- Performance – caching encouraged; minimal allocations on hot paths.
4. Interfaces
All namespaces below are
Psr\Workflow
. Implementations must declaredeclare(strict_types=1);
and type-hint accurately.
<?php declare(strict_types=1); namespace Psr\Workflow; interface ContextInterface { /** Unique key, stable across process restarts. */ public function getId(): string; /** * Arbitrary key-value map for routing decisions (e.g., project/type/tenant). * Implementations SHOULD ensure values are scalar or JSON-serializable. * * @return array<string, scalar|array|null> */ public function getAttributes(): array; } interface ActorInterface { public function getId(): string; /** @return list<string> */ public function getRoles(): array; } interface SubjectInterface { /** MUST return the persisted single active status. */ public function getStatus(): string; /** MUST not persist before Engine finishes; Engine coordinates persistence. */ public function setStatus(string $status): void; } interface ExecutionContextInterface { public function getSubject(): Subject; public function getActor(): ?Actor; /** Opaque runtime data (form inputs, correlation ids, etc.). */ public function getPayload(): array; public function getContext(): ?Context; }
4.1 Definitions
<?php declare(strict_types=1); namespace Psr\Workflow; interface StatusInterface { public function getKey(): string; /** Optional reporting category (e.g., todo|in_progress|done). */ public function getCategory(): ?string; } interface TransitionInterface { public function getKey(): string; /** @return non-empty-list<string> */ public function getFrom(): array; public function getTo(): string; /** @return list<string> Aliases/ids resolving to Condition services */ public function getConditionRefs(): array; /** @return list<string> Aliases/ids resolving to Validator services */ public function getValidatorRefs(): array; /** @return list<string> Aliases/ids resolving to PostFunction services */ public function getPostFunctionRefs(): array; /** Optional UI screen reference; not interpreted by PSR‑WF. */ public function getScreenRef(): ?string; /** @return list<string> TriggerInterface identifiers (optional) */ public function getTriggers(): array; } interface WorkflowInterface { public function getKey(): string; /** @return list<StatusInterface> */ public function getStatuses(): array; /** @return list<TransitionInterface> */ public function getTransitions(): array; public function getInitialStatus(): string; } interface WorkflowSchemeInterface { /** Map Context+Subject to a workflow key. MUST be pure/deterministic. */ public function resolveWorkflowKey(ContextInterface $context, SubjectInterface $subject): string; } interface WorkflowRegistryInterface { public function getWorkflow(string $workflowKey): WorkflowInterface; public function getScheme(): ?WorkflowScheme; }
4.2 Runtime SPI
<?php declare(strict_types=1); namespace Psr\Workflow; interface ConditionInterface { public function isAllowed(ExecutionContextInterface $ctx, Transition $transitionInterface): bool; } interface ValidatorInterface { /** * MUST throw ValidationException on failure. * MUST be side-effect free (no persistence/IO beyond read-only). */ public function validate(ExecutionContextInterface $ctx, TransitionInterface $transition): void; } interface PostFunctionInterface { /** * Executed AFTER status change but WITHIN the same domain transaction. * MUST be idempotent or use operation keys to prevent duplicates. */ public function execute(ExecutionContextInterface $ctx, TransitionInterface $transition): void; } interface TriggerInterface { /** Hints if a transition should be attempted by a scheduler/reactor. */ public function shouldFire(ExecutionContextInterface $ctx, TransitionInterface $transition): bool; } interface MarkingStoreInterface { public function getStatus(SubjectInterface $subject): string; public function setStatus(SubjectInterface $subject, string $status): void; } interface Engine { public function can(ExecutionContext $ctx, string $transitionKey): bool; /** * Executes the transition atomically or throws. * MUST: * 1) re-evaluate Conditions, * 2) run Validators (short-circuit on first failure), * 3) set new status via MarkingStore, * 4) run PostFunctions in configured order, * 5) emit events (PSR-14) and audit logs (PSR-3). */ public function apply(ExecutionContextInterface $ctx, string $transitionKey): void; /** Returns transition keys available after Conditions pass. */ public function getAvailableTransitions(ExecutionContextInterface $ctx): array; }
5. Execution Semantics
5.1 Single Marking
At any time, a Subject has exactly one active Status (string key).
5.2 can()
Evaluation
Engine::can()
MUST return true iff:
Subject.status ∈ Transition.from
for the resolved workflow, and- all Conditions return
true
for the givenExecutionContext
.
Implementations MAY cache condition results per (subject, transition, actor, context, payload hash) during a request.
5.3 apply()
Semantics
Engine::apply()
MUST be atomic w.r.t. domain persistence boundary. The minimal required order:
- Resolve Workflow and Transition deterministically.
- Re-check Conditions (TOCTOU protection).
- Execute Validators in configured order; abort on first failure with
ValidationException
. - Record current
from
andto
. - Persist new status via
MarkingStore::setStatus()
. - Execute PostFunctions in configured order.
- Emit events via PSR‑14 (see §6) and log via PSR‑3 (info/debug/audit).
- On any throwable in steps 3, 5, 6: rollback status change and all side-effects in the application transaction.
Implementations SHOULD provide idempotency by accepting an optional operation id in ExecutionContext::getPayload()
and de-duplicating PostFunctions.
5.4 Ordering
- Conditions → Validators → Status change → PostFunctions → Events.
- Within each list, order MUST be deterministic and configuration-controlled.
5.5 Concurrency & Re-check
Implementations MUST re-evaluate Conditions immediately before persisting the status and SHOULD use proper transaction isolation (at least REPEATABLE READ) to minimize lost updates.
6. Events (PSR‑14)
Implementations MUST dispatch the following events with immutable payload objects:
WorkflowGuarded
(before/after conditions evaluation),WorkflowValidationFailed
(per validator or aggregate),WorkflowTransitionStarted
,WorkflowStatusChanged
(includes workflowKey, transitionKey, from, to, subjectId, actorId, contextId, payload snapshot, timestamp),WorkflowPostFunctionsCompleted
,WorkflowTransitionCompleted
.
Events MUST NOT leak mutable references to Subject. Payload SHOULD include correlation id when provided.
7. Error Handling
Standard exception hierarchy (implementations SHOULD extend these):
<?php declare(strict_types=1); namespace Psr\Workflow; interface WorkflowException extends \Throwable {} final class InvalidDefinitionException extends \RuntimeException implements WorkflowException {} final class TransitionNotFoundException extends \RuntimeException implements WorkflowException {} final class IllegalStateException extends \RuntimeException implements WorkflowException {} final class UnauthorizedTransitionException extends \RuntimeException implements WorkflowException {} class ValidationException extends \RuntimeException implements WorkflowException {}
Semantics:
- InvalidDefinition – inconsistent graphs, unknown references, missing initial status.
- TransitionNotFound – key not in workflow or not from current status.
- IllegalState – engine invariants violated (e.g., multiple statuses detected).
- UnauthorizedTransition – any Condition denied.
- ValidationException – Validator rejected runtime data.
All errors MUST leave Subject status unchanged.
8. Security Considerations
- Least privilege: default-deny Conditions; explicit allow-lists for strategies.
- Auditability: implementations SHOULD log
who/what/when/from→to/payload
(PSR‑3). - Idempotency: accept and honor
operationId
in payload to avoid duplicates. - Input validation: Validators MUST run before persisting status.
- Side-effects isolation: PostFunctions MUST be retry-safe or compensatable.
9. Caching (Optional)
Implementations MAY cache:
- parsed definitions (workflows, transitions, schemes);
- authorization and condition results per request;
- resolved workflow key per (context, subject type).
Caches MUST be invalidated on definition publish/rollout. Prefer PSR‑6/16.
10. Versioning & Migration
- Workflows SHOULD be versioned (
issue_default@v3
). - Registries SHOULD support draft/published states and atomic publish.
- Migration helpers MAY rewrite statuses for open Subjects during rollout.
- Schemes MAY route old contexts to older versions until migration completes.
11. Conformance
A library is PSR‑WF compliant if:
- It provides concrete classes implementing all interfaces in §4.
- It follows Execution Semantics (§5) and Error Handling (§7).
- It dispatches Events (§6) with the prescribed timing.
- It ships contract tests that third-party strategies can run against.
Suggested test matrix:
- Positive/negative
can()
cases. - Validator failure short-circuits PostFunctions.
- PostFunction failure rolls back status.
- Deterministic ordering respected.
- Concurrency re-check prevents stale transitions.
12. Reference Mappings (Non-normative)
- Symfony: map to services with tags; Doctrine for transactions; EventDispatcher bridge for PSR‑14.
- Laravel: ServiceProvider bindings; Gates/Policies as Conditions; Events; Eloquent transactions.
- Jira-style parity: Conditions/Validators/PostFunctions/Triggers/Schemes correspond 1:1 with semantics preserved.
13. Minimal Example (Non-normative)
$ctx = new DefaultExecutionContext( subject: $issue, actor: $currentUser, payload: ['operationId' => 'op-123', 'comment' => 'start work'], context: new ProjectContext('APP:ISSUE', ['project' => 'APP', 'type' => 'Bug']) ); if ($engine->can($ctx, 'start_progress')) { $engine->apply($ctx, 'start_progress'); }
14. Backwards Compatibility & Future Work
This spec intentionally targets single-marking FSM. A future PSR may define multi-marking (parallel places) and workflow nets, and standardized configuration formats (YAML/JSON) as separate, optional standards.
15. License
This document is licensed under the GPL-3.0 license. See LICENSE.
End of PSR‑WF Draft v1.0.0