PHPackages                             milpa/workflow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. milpa/workflow

ActiveLibrary[Framework](/categories/framework)

milpa/workflow
==============

ORM-backed data-driven state machine for the Milpa PHP framework: states, transitions and approval gates with evidence, plus a VerifierInterface bridge into the core verification seam.

v0.1.5(3w ago)0152↑90%2Apache-2.0PHPPHP &gt;=8.3CI passing

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/getmilpa/workflow)[ Packagist](https://packagist.org/packages/milpa/workflow)[ RSS](/packages/milpa-workflow/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (27)Versions (7)Used By (2)

 [   ![Milpa](https://raw.githubusercontent.com/getmilpa/core/main/art/lockup/milpa-lockup-v-color-light.svg)  ](https://github.com/getmilpa)

Milpa Workflow
==============

[](#milpa-workflow)

> The **ORM-backed, data-driven state machine** of the Milpa PHP framework: states, transitions and approval **gates** — with evidence attachments — configured in the database instead of hardcoded, plus `StateMachineVerifier`, a bridge onto `milpa/core`'s `VerifierInterface`.

[![CI](https://github.com/getmilpa/workflow/actions/workflows/ci.yml/badge.svg)](https://github.com/getmilpa/workflow/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/40b328163f3207b976641d8130a144f24d3ad01780810254692ebc87f047d169/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d696c70612f776f726b666c6f772e737667)](https://packagist.org/packages/milpa/workflow)[![PHP](https://camo.githubusercontent.com/ca03f11ea27dac4dedc8ad56a7bdfc4a9ff5feb825055f9d2983616115076607/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135253230382e332d3737376262342e737667)](https://www.php.net/)[![License](https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667)](LICENSE)[![Docs](https://camo.githubusercontent.com/c6dc6a3411e15b0ac7cc4583e8e6a8144181caedb82f5d98753353decda06d77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d4150492532307265666572656e63652d626c75652e737667)](https://getmilpa.github.io/workflow/)

`milpa/workflow` is the family's first **tier-2** package: Doctrine ORM is an honest runtime dependency (used only through `findBy`/`findOneBy`/`QueryBuilder` on the package's own entities), not a zero-dep primitive like `milpa/core`. Everything else about the family still holds — no product coupling, no concrete container, opaque identity.

Install
-------

[](#install)

```
composer require milpa/workflow
```

Quick example
-------------

[](#quick-example)

```
use Milpa\Workflow\StateMachine\DataDrivenStateMachine;
use Milpa\Workflow\StateMachine\TransitionContext;
use Milpa\Workflow\Gates\DefaultGateEvaluator;
use Milpa\Workflow\Services\GatePassageService;

// $em is a Doctrine\ORM\EntityManagerInterface wired to a schema that has run
// doctrine/orm's schema tool over Milpa\Workflow\Entities\*; $gate is a
// GateDefinition your domain configured (e.g. persisted via a migration/seeder).
$stateMachine = new DataDrivenStateMachine($em, new DefaultGateEvaluator());
$gateService = new GatePassageService($em);

// Ask whether 'lead' -> 'qualified' is currently possible for domain 'opportunity'.
$context = new TransitionContext(actorId: 7, actorRole: 'sales', entityId: 99, domain: 'opportunity');
$result = $stateMachine->canTransition('opportunity', 'lead', 'qualified', $context);

if (!$result->isPassed()) {
    // $result->missingFields / $result->missingEvidence explain exactly what's missing.
}

// Request (and later approve) a gate passage against a GateDefinition your domain
// configured (e.g. `$em->getRepository(GateDefinition::class)->findOneBy([...])`).
// Principals are opaque strings the engine never resolves — the consuming product
// owns identity.
$passage = $gateService->requestPassage($gate, 'opportunity', 99, requesterId: 'member:7');
$gateService->approvePassage($passage, approverId: 'member:12', notes: 'fit score confirmed');
```

What it does — and doesn't
--------------------------

[](#what-it-does--and-doesnt)

- **`Contracts\StateMachineInterface`** (implemented by `StateMachine\DataDrivenStateMachine`) — reads `Entities\StateDefinition`/`TransitionDefinition`/`GateDefinition` from the database to answer `canTransition()`, `getAvailableTransitions()`, `transition()`, and lookups by domain/code. No transition graph is hardcoded in PHP.
- **`Contracts\GateServiceInterface`** — request/approve/reject/waive an `Entities\GatePassage`, with the anti-self-approval constraint enforced at the service layer (`Exceptions\SelfApprovalException`). Two implementations ship: `Services\GatePassageService` (Doctrine-backed, append-only persisted rows) and `Services\InMemoryGateService` (zero-DB, for consumers with no `EntityManager`) — see "Gate services: Doctrine and in-memory" below.
- **`Verification\StateMachineVerifier`** — runs the gate machinery through `milpa/core`'s `VerifierInterface`: a generic `VerificationRequest`/`VerificationContext`in, a `VerificationResult` out, bridged losslessly through `StateMachine\GateResult`. This is the monorepo's only automated `VerifierInterface` implementation — the deterministic counterpart to a human/agent-supplied verification.
- **Opaque identity everywhere a principal is stored**: `Entities\Evidence::$uploadedBy`and `Entities\GatePassage::$requestedBy`/`$approvedBy` are plain strings such as `"member:42"`. The engine never resolves them to an entity — the consuming product owns identity resolution (see [`ADR-001`](https://github.com/getmilpa/core) / design note D9).
- **No product coupling.** The polymorphic `entity_type`/`entity_id` pair on `GatePassage` lets any domain (opportunity, project, invoice, ...) register its own states/transitions/gates without this package knowing about it.

Gate services: Doctrine and in-memory
-------------------------------------

[](#gate-services-doctrine-and-in-memory)

`Contracts\GateServiceInterface` has two implementations, picked by whether the consumer has a Doctrine `EntityManagerInterface` at all:

- **`Services\GatePassageService`** — the original, Doctrine-backed implementation. `persist()`/`flush()`s each `Entities\GatePassage` as an append-only row and can answer `getApprovedPassagesForEntity()` with a real `QueryBuilder` query. Requires an `EntityManagerInterface` in its constructor.
- **`Services\InMemoryGateService`** — a non-Doctrine implementation for zero-DB / event-sourced consumers (e.g. `milpa/orchestrator` replaying its own append-only event log) that have no `EntityManagerInterface` to construct `GatePassageService` with. Requires **zero** constructor arguments (an optional `AuditLoggerInterface` is the only parameter). It returns the same `Entities\GatePassage` entity `GatePassageService`does — `GatePassage`'s constructor/setters are plain PHP, so `new GatePassage()` works without Doctrine as long as its Doctrine-generated `getId()` is never read; this class uses `GatePassage::getUuid()` wherever a stable identifier is needed instead. State (recorded approvers, the approved-passages-per-entity index) lives only for the lifetime of the service instance — keep one alive per request/process if you need `ApprovalPolicy::DUAL`'s two-distinct-approvers count to span more than one call. It is also the first implementation to actually honor a gate's `ApprovalPolicy`: `DUAL` requires two distinct approver principals across two `approvePassage()` calls before the passage leaves `REQUESTED`; `SINGLE`, `QUORUM`, and `AUTO` all resolve on the first approval (see the class DocBlock for why `QUORUM`/`AUTO` fall back that way).

Both implementations enforce the same D9 self-approval guard on `approvePassage()`(`Exceptions\SelfApprovalException`) and the same waivability guard on `waiveGate()`(`Exceptions\NonWaivableGateException`) — pick the implementation that matches your persistence story; the interface (and every other collaborator, like `Verification\StateMachineVerifier`) doesn't care which one it's talking to.

Namespace layout
----------------

[](#namespace-layout)

Public contracts live under `Contracts/` (not `Interfaces/`, which is `milpa/core`'s established, already-published convention) — this package follows the family convention adopted from ola 2 onward: new packages use `Contracts/` for interfaces/value-objects that form the package's public seam, while concern-scoped strategy interfaces stay colocated with their concern (`StateMachine\GateEvaluatorInterface` lives in `StateMachine/`, next to the `GateResult`/`TransitionContext` types it operates on).

Testing
-------

[](#testing)

The test suite is standalone: it exercises the state machine's transition/gate logic and the `StateMachineVerifier` bridge against **stub implementations** of `Contracts\StateMachineInterface`/`StateMachine\GateEvaluatorInterface` and hand-built entity graphs — no live database, no Doctrine bootstrap. The two Doctrine-backed services (`StateMachine\DataDrivenStateMachine`'s repository lookups and `Services\GatePassageService`'s persistence) talk to `EntityManagerInterface` through `findBy`/`findOneBy`/`QueryBuilder` only; nothing in this package requires a schema migration or a running MySQL to validate its logic.

Requirements
------------

[](#requirements)

- PHP **≥ 8.3**
- `doctrine/orm` **^3.0** (the package's one runtime dependency beyond `milpa/core`)
- `milpa/core` — `VerifierInterface`, the `VerificationRequest`/`VerificationContext`/ `VerificationResult` value objects, and `Support\UuidGenerator`

Documentation
-------------

[](#documentation)

**Full API reference: [getmilpa.github.io/workflow](https://getmilpa.github.io/workflow/)**— generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing
------------

[](#contributing)

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues via [SECURITY.md](SECURITY.md), and note that this project follows a [Code of Conduct](CODE_OF_CONDUCT.md).

License
-------

[](#license)

[Apache-2.0](LICENSE) © Rodrigo Vicente - TeamX Agency.

---

Milpa is designed, built, and maintained by **[Rodrigo Vicente - TeamX Agency](https://teamx.agency/?utm_source=github&utm_medium=readme&utm_campaign=milpa&utm_content=workflow)**.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance93

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community15

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 77.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

6

Last Release

24d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1993784?v=4)[rodrigomx](/maintainers/rodrigomx)[@rodrigomx](https://github.com/rodrigomx)

---

Top Contributors

[![rodrigoteamx](https://avatars.githubusercontent.com/u/269849276?v=4)](https://github.com/rodrigoteamx "rodrigoteamx (14 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (4 commits)")

---

Tags

doctrineframeworkphpstate-machinephpframeworkormworkflowstate-machineapprovalstransitionsmilpa

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/milpa-workflow/health.svg)

```
[![Health](https://phpackages.com/badges/milpa-workflow/health.svg)](https://phpackages.com/packages/milpa-workflow)
```

###  Alternatives

[api-platform/doctrine-orm

Doctrine ORM bridge

314.8M107](/packages/api-platform-doctrine-orm)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12017.1k](/packages/rcsofttech-audit-trail-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
