dereuromark/cakephp-workflow

State machine and workflow engine for CakePHP with PHP 8 Attributes, YAML support, and admin UI

Maintainers

Package info

github.com/dereuromark/cakephp-workflow

Homepage

Type:cakephp-plugin

pkg:composer/dereuromark/cakephp-workflow

Statistics

Installs: 1 424

Dependents: 1

Suggesters: 0

Stars: 2

Open Issues: 0

0.2.1 2026-05-23 23:29 UTC

This package is auto-updated.

Last update: 2026-05-30 13:13:49 UTC


README

CI codecov Latest Stable Version Minimum PHP Version License Coding Standards

This branch is for CakePHP 5.2+. See version map for details.

State machine and workflow engine for CakePHP with PHP 8 Attributes, YAML/NEON config support, and admin UI.

Tip

Try the live demo: https://sandbox.dereuromark.de/workflow-sandbox

Requirements

  • CakePHP 5.2+

Installation

composer require dereuromark/cakephp-workflow

For server-side diagram exports via /workflow/workflows/draw in svg / png formats, install GraphViz and make sure the dot binary is available on the host. Widget exports in app pages do not require GraphViz.

Load the plugin:

bin/cake plugin load Workflow

Run migrations:

bin/cake migrations migrate --plugin Workflow

The generic foreign_key column is polymorphic and defaults to integer; set the shared Polymorphic.type key to biginteger for large-id apps, or uuid / binaryuuid for non-integer keys. UUID / char primary keys are fully supported — no code changes needed. See Installation: Entity id type.

Configuration

Configure the plugin in your config/app.php:

'Workflow' => [
    'adminAccess' => function (\Cake\Http\ServerRequest $request): bool {
        $identity = $request->getAttribute('identity');

        return $identity !== null && in_array('admin', (array)$identity->roles, true);
    },
    'loader' => [
        'namespaces' => [
            'App\\Workflow',
        ],
        'configPath' => CONFIG . 'workflows' . DS,
    ],
    'logging' => true,
    'locking' => true,
    'timeouts' => true,
    'lockDuration' => 30,
    'adminBackUrl' => ['plugin' => false, 'controller' => 'Dashboard', 'action' => 'index'],
    'adminActorResolver' => function (string $userId, ?\Workflow\Model\Entity\WorkflowTransition $transition = null): string {
        return 'Admin #' . $userId;
    },
],

The admin UI is fail-closed by default: you must provide Workflow.adminAccess to expose /admin/workflow/.... Manual admin actions also record actor and context metadata; when you still rely on legacy session auth, the admin logger falls back to Auth.User.id automatically.

Defining Workflows

Using PHP 8 Attributes (Recommended)

Create state classes in your namespace:

<?php
namespace App\Workflow\Order;

use Workflow\Attribute\StateMachine;
use Workflow\State\AbstractState;

#[StateMachine(name: 'order', table: 'Orders', field: 'state')]
abstract class OrderState extends AbstractState
{
}
<?php
namespace App\Workflow\Order;

use Workflow\Attribute\Command;
use Workflow\Attribute\FinalState;
use Workflow\Attribute\Guard;
use Workflow\Attribute\InitialState;
use Workflow\Attribute\Transition;

#[InitialState]
#[Transition(to: PaidState::class, name: 'pay', happy: true)]
class PendingState extends OrderState
{
    #[Guard('pay')]
    public function ensurePayable(): bool|string
    {
        return (float)$this->getEntity()?->get('total') > 0
            ? true
            : 'Order total must be positive';
    }

    #[Command('pay')]
    public function markPaymentCaptured(): void
    {
        $this->getEntity()?->set('payment_captured', true);
    }
}
<?php
namespace App\Workflow\Order;

use Workflow\Attribute\FinalState;
use Workflow\Attribute\OnEnter;

#[FinalState]
class PaidState extends OrderState
{
    #[OnEnter]
    public function sendReceipt(): void
    {
        // Runs after the entity enters the paid state.
    }
}

Using NEON or YAML

Install the optional parser you want:

  • NEON: composer require nette/neon
  • YAML: composer require symfony/yaml

Create workflow files in config/workflows/:

order:
  table: Orders
  field: state
  states:
    pending:
      initial: true
    paid:
      color: '#00AA00'
    completed:
      final: true
  transitions:
    pay:
      from: [pending]
      to: paid
      happy: true
    complete:
      from: [paid]
      to: completed

Using the Workflow

Add the behavior to your table:

public function initialize(array $config): void
{
    $this->addBehavior('Workflow.Workflow', [
        'workflow' => 'order',
    ]);
}

Apply transitions:

$behavior = $this->Orders->getBehavior('Workflow');

if ($behavior->canTransition($order, 'pay')) {
    // Atomic: applies transition, saves entity, logs - all in one transaction
    $result = $behavior->transition($order, 'pay', ['user_id' => $userId]);
}

See the documentation for the full API.

Embedding Workflow Diagrams In App Pages

For app-facing pages you often want a compact diagram preview, not the full admin screen.

Load Mermaid with the helper toolkit:

<?= $this->Workflow->includeMermaid([
    'startOnLoad' => false,
    'toolkit' => true,
]) ?>

Render a compact workflow widget with:

  • current-state highlighting
  • optional current-state centering
  • code toggle
  • fullscreen modal
  • client-side SVG export of the rendered Mermaid graph
  • client-side PNG export derived from the rendered Mermaid graph
  • Mermaid source export
<?= $this->Workflow->widget($definition, [
    'title' => 'Order workflow',
    'currentState' => $order->state,
    'showDetails' => true,
    'detailMarkers' => 'ascii',
    'export' => ['svg', 'png', 'mmd'],
]) ?>

For detailed helper options and canonical server-side draw exports, see the View Helper integration docs.

  • exportFilename
  • minWidth
  • maxHeight
  • modalMinWidth

SVG export uses a standalone serializer, so downloaded files get explicit dimensions from the rendered viewBox instead of keeping Mermaid's responsive width="100%" markup. This makes saved SVGs much more usable in external tools.

PNG export rasterizes that same standalone SVG into a canvas with a white background, which is useful for docs, tickets, and tools that do not render SVG cleanly.

Drift Safety

Changing a workflow while records exist can leave records in a state that no longer exists. This is handled out of the box, with no configuration:

  • Graceful degradation: orphaned records never crash reads, display, or the admin UI — they render as a neutral "unknown" state, and transitioning one returns a clear blocked result.
  • Detection: the admin Orphans view (/admin/workflow/orphans) and workflow validate --check-data list records whose state is no longer defined.
  • Remediation: move them forward interactively, or headlessly:
bin/cake workflow validate order --check-data          # report orphaned records
bin/cake workflow migrate order --map legacy:pending   # move orphaned records forward

See Drift Safety for details.

CLI Commands

bin/cake workflow init order Orders    # Scaffold new workflow
bin/cake workflow list                 # List all workflows
bin/cake workflow show order           # Show workflow details
bin/cake workflow validate             # Validate definitions
bin/cake workflow migrate order        # Move orphaned records to valid states

Features

  • PHP 8 Attributes or NEON/YAML definitions
  • Guards, commands, and lifecycle callbacks
  • Audit logging with user tracking
  • Pessimistic locking for concurrent transitions
  • Automatic timeouts
  • Drift safety: orphaned records never crash, with detection and forward migration
  • Admin UI with Mermaid.js diagrams
  • CLI tools for management and validation

Documentation

Full documentation: https://dereuromark.github.io/cakephp-workflow/