Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ on:
- 'phpstan.neon.dist'
- '.github/workflows/phpstan.yml'

pull_request:
paths:
- '**.php'
- 'phpstan.neon.dist'
- '.github/workflows/phpstan.yml'

jobs:
phpstan:
name: phpstan
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ on:
- 'composer.json'
- 'composer.lock'



pull_request:
paths:
- '**.php'
- '.github/workflows/run-tests.yml'
- 'phpunit.xml.dist'
- 'composer.json'
- 'composer.lock'


jobs:
test:
runs-on: ${{ matrix.os }}
Expand All @@ -18,7 +27,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.4, 8.3]
php: [8.5, 8.4, 8.3]
stability: [prefer-lowest, prefer-stable]

name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
Expand Down
102 changes: 102 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,108 @@ All notable changes to `workflow-engine-core` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v2.0.0 - 2026-09-12

Fixes several cases where the engine silently did the wrong thing and still
reported success — the failure mode a workflow engine can least afford.

**Why 2.0.0 and not 1.1.0:** `v1.0.0` was tagged as a stable release, so the
corrections below — which change public class names and runtime behaviour — are
breaking changes under semver and require a major bump. Every one is listed with
its migration. If you are on `v0.0.4-alpha` or `v1.0.0`, read the *Changed*
section before upgrading.

### Fixed

- **Compound conditions no longer evaluate to the wrong branch.** `ConditionEvaluator`
had no `&&`/`||` support, but instead of rejecting them it swallowed the operator
into the right-hand side and fell back to a string comparison — so
`order.total > 1000 && order.vip === true` returned **true** for a 500 dollar
order, with no exception and nothing in the logs. Boolean operators, parentheses
and negated groups are now supported, and a malformed expression throws
`InvalidWorkflowDefinitionException` instead of collapsing to a boolean.
- **`resume()` works on a failed workflow.** Recovering a failed instance — the
documented recovery path — threw `Cannot transition workflow from 'failed' to
'failed'`. Three defects chained: the executor never lifted `FAILED` back to
`RUNNING`; the final `FAILED -> COMPLETED` hop was rejected; and the resulting
exception was raised *inside* the failure handler, destroying the original cause.
`FAILED -> RUNNING` is now a legal transition, resume retries the step that
failed, and the failure handler can no longer mask the real error.
- **Multi-root workflows no longer drop a branch.** A definition with two
independent entry points executed only one of them and still reported
`COMPLETED` (with progress stuck below 100%). Every root now runs.
- **Unreachable steps are rejected at parse time.** A group of steps reachable
only from each other could never execute, yet the workflow reported success.
- **Relational comparisons against a missing key are `false`.** `null` coerces to
`0` in PHP, so `missing.key < 1000` was *true* and steps gated on data that was
never set would run.
- **`start()` no longer overwrites an existing instance.** Reusing a workflow ID
silently discarded the earlier run's state and history; it now throws
`InvalidWorkflowStateException`.
- **Blocked workflows park in `WAITING`.** A workflow whose steps were all blocked
on unmet prerequisites stayed in `RUNNING` forever, indistinguishable from one
still executing.
- **`DelayAction` honours `minutes` and `hours`.** Both were documented but never
read, so `delay(hours: 2)` silently paused for the one second default.
- **`HttpAction` reports a missing cURL extension** as a step failure instead of a
fatal "undefined function" error.

### Changed

- **BREAKING: `EmailAction` is now `FakeEmailAction`.** It never sent email, but
returned `'status' => 'sent'` — a workflow could show a delivered confirmation
that did not exist. The payload is now explicitly `'sent' => false, 'mock' => true`,
and it logs a warning. *Migration: implement `WorkflowAction` with your own mail
transport for real delivery.*
- **BREAKING: `WorkflowBuilder::email()` is now `fakeEmail()`**, for the same
reason. *Migration: rename the call, or switch to your own action.*
- **BREAKING: `ConditionAction` no longer accepts `on_true` / `on_false`.** They
were read but never consumed by the engine, so the documented branching did not
exist. *Migration: branch with a `condition` on the transition instead.*
- **BREAKING: `ConditionAction` uses the shared condition grammar.** It previously
carried its own parser accepting `=`, `is` and `is not`, which no other part of
the engine understood. *Migration: use `===`, `==`, `!=` etc.*
- **BREAKING: `FAILED` is no longer a terminal state** — it can transition to
`RUNNING` (resume) or `CANCELLED`. Code asserting that failed workflows are
immutable needs updating.
- `WorkflowState::isFinished()` still reports `true` for `FAILED`; use
`canTransitionTo()` to test whether an instance can still move.

### Added

- `Storage\InMemoryStorage` now **ships with the package**. Previously the only
implementation lived in `tests/` under `autoload-dev`, so the library could not
run a workflow out of the box without first writing an adapter.
- `WorkflowDefinition::getFirstSteps()` returns every entry point.
- `Support\Arr::has()` distinguishes an absent key from one holding `null`.
- Boolean operators (`&&`, `||`), parentheses and negated groups in conditions.
- `SECURITY.md` with a disclosure process and scope notes.
- Regression tests for every issue above (116 -> 161 tests).

### CI

- **PHP 8.5 added to the test matrix.** The library accepts `php: ^8.3`, which
admits 8.5, but it was never tested there. (Installing on 8.5 became possible
once #57 widened the dev tooling constraints.)
- `run-tests.yml` and `phpstan.yml` now run on **pull requests**, not just pushes.
Combined with `dependabot-auto-merge.yml` auto-merging minor and patch bumps,
dependency updates could previously reach `main` without the suite ever running
against the merge result.
- Removed a stale PHPStan `ignoreErrors` pattern that no longer matched anything.

### Docs

- Documented the condition grammar, including the two rules that stop a mistyped
condition from silently routing a workflow the wrong way.
- Corrected the built-in actions table: it documented a `body` key for
`EmailAction` and `HttpAction` that neither read, `minutes`/`hours` for
`DelayAction` that were ignored, and branching for `ConditionAction` that did
not exist.
- Fixed the PHP attribute examples, whose inline comments claimed retries and
timeouts that the attributes do not actually perform — the engine still does not
read them (this is noted in the README).
- Archived the completed `PLAN.md` to `docs/PLAN-2025-refactor.md`.

## v1.0.0 - 2026-09-12

First stable release.
Expand Down
35 changes: 20 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Project context for Claude Code and AI-assisted development.

**workflow-engine-core** is a framework-agnostic PHP workflow engine. Zero production dependencies. PHP 8.3+. MIT licensed.

Status: **v0.0.2-alpha** — active development, not production-ready.
Status: **v2.0.0** — stable. Breaking changes follow semver and are documented in CHANGELOG.md.

Related package: `solution-forest/workflow-engine-laravel` (Laravel integration layer).

Expand Down Expand Up @@ -50,31 +50,36 @@ WorkflowBuilder → WorkflowDefinition → WorkflowEngine → Executor → Actio
| Namespace | Purpose |
|-----------|---------|
| `Core\` | WorkflowEngine, WorkflowBuilder, Executor, StateManager, WorkflowInstance, WorkflowDefinition, WorkflowContext, ActionResult, Step, DefinitionParser, ActionResolver |
| `Actions\` | BaseAction, LogAction, EmailAction, HttpAction, DelayAction, ConditionAction |
| `Actions\` | BaseAction, LogAction, FakeEmailAction, HttpAction, DelayAction, ConditionAction |
| `Contracts\` | WorkflowAction, StorageAdapter, EventDispatcher, Logger |
| `Attributes\` | WorkflowStep, Retry, Timeout, Condition |
| `Events\` | WorkflowStartedEvent, WorkflowCompletedEvent, WorkflowFailedEvent, WorkflowCancelledEvent, StepCompletedEvent, StepFailedEvent, StepRetriedEvent |
| `Exceptions\` | WorkflowException (base), InvalidWorkflowDefinitionException, InvalidWorkflowStateException, ActionNotFoundException, StepExecutionException, WorkflowInstanceNotFoundException |
| `Support\` | NullLogger, NullEventDispatcher, SimpleWorkflow, Uuid, Timeout, ConditionEvaluator, Arr |
| `Storage\` | InMemoryStorage (ships with the package; non-durable) |

### State Machine

```
PENDING → RUNNING → COMPLETED
↓ ↑
FAILED WAITING
↓ ↑
FAILED ← PAUSED
CANCELLED ← (any non-terminal state)
PENDING ──→ RUNNING ──→ COMPLETED (terminal)
↓ ↑
WAITING / PAUSED
↓ ↑
FAILED ──→ RUNNING (resume retries the failed step)
CANCELLED (terminal)
```

**Valid transitions (enforced at runtime):**
- `PENDING` → `RUNNING`, `FAILED`, `CANCELLED`
- `RUNNING` → `WAITING`, `PAUSED`, `COMPLETED`, `FAILED`, `CANCELLED`
- `WAITING` → `RUNNING`, `FAILED`, `CANCELLED`
- `PAUSED` → `RUNNING`, `FAILED`, `CANCELLED`
- Terminal states (`COMPLETED`, `FAILED`, `CANCELLED`) → no transitions allowed
- `FAILED` → `RUNNING` (recovery via `resume()`), `CANCELLED`
- Terminal states (`COMPLETED`, `CANCELLED`) → no transitions allowed

`FAILED` is recoverable: `resume()` returns the instance to `RUNNING` and retries
the step that failed. A workflow blocked on unmet prerequisites parks in `WAITING`.

Invalid transitions throw `InvalidWorkflowStateException`.

Expand Down Expand Up @@ -154,15 +159,15 @@ $engine->cancel($instanceId, 'reason');
## CI/CD

GitHub Actions workflows:
- `run-tests.yml` — Matrix: PHP 8.3/8.4 × prefer-lowest/prefer-stable
- `phpstan.yml` — Static analysis on .php changes
- `run-tests.yml` — Matrix: PHP 8.3/8.4 × prefer-lowest/prefer-stable (runs on push **and** pull_request)
- `phpstan.yml` — Static analysis on .php changes (runs on push **and** pull_request)
- `fix-php-code-style-issues.yml` — Auto-format with Pint on push
- `update-changelog.yml` — Auto-update CHANGELOG on release
- `dependabot-auto-merge.yml` — Auto-merge minor/patch dependency updates

## File Counts

- 46 source files in `src/`
- 25 test files in `tests/`
- 93 tests, 224+ assertions
- 47 source files in `src/`
- 27 test files in `tests/`
- 161 tests, 363+ assertions
- PHPStan level 6 compliance
Loading