free-agent/workflow-bundle
Free Agent's Workflow Bundle brings structured workflow management to PHP applications. Define multi-step processes, manage transitions, and track state in one lightweight package. A practical choice for projects needing simple, configurable workflow orchestration.
Reality check first: free-agent/workflow-bundle is a Symfony2 bundle, not a Laravel package. It is archived (2 stars, 0 dependents), so there is no path where you composer require it into a Laravel app and get a working service provider. Treat this assessment as "how would I use this if I inherited a Symfony 2.x codebase" plus "what would I actually do in Laravel instead."
If you are in the legacy Symfony app it was written for:
composer require free-agent/workflow-bundle — expect PHP 5.6/7.x-era constraints; check composer.json before assuming it installs on modern PHP.app/AppKernel.php (new FreeAgent\WorkflowBundle\FreeAgentWorkflowBundle()). There is no auto-discovery; Symfony2 bundles are wired explicitly.README.md (the config tree and any example YAML), DependencyInjection/Configuration.php (the authoritative schema for defining workflows), the main workflow/registry service class, and the interface your entity must implement to be a workflow subject. The tests are the best documentation for guard/callback semantics.config.yml for a single entity with a small state set — draft → submitted → approved — and one transition. Call the workflow service from a controller, persist the entity, and assert the state changed and that an invalid transition throws.What a Laravel developer should actually do: install symfony/workflow (the standalone, maintained component — this bundle is essentially a predecessor of it) or hand-roll an enum + a transition service. The bundle's ideas port cleanly; its code does not.
Declarative definitions beat inline if chains. The bundle's model is a named workflow containing places (states) and transitions, defined in config rather than scattered across controllers. In Laravel, mirror this with a config array or a WorkflowDefinition value object registered per model class:
// config/workflows.php
Order::class => [
'places' => ['draft', 'submitted', 'approved', 'rejected'],
'transitions' => [
'submit' => ['from' => ['draft'], 'to' => 'submitted'],
'approve' => ['from' => ['submitted'], 'to' => 'approved'],
'reject' => ['from' => ['submitted'], 'to' => 'rejected'],
],
],
Subject interface on the model. The bundle expects your entity to expose a state property and implement a marker interface. In Laravel that becomes a HasWorkflow trait on the Eloquent model with a workflowState() accessor reading a state column.
Thin controllers, fat transition service. The bundle's service is a registry/dispatcher; don't call it from Blade or from five places in a controller. Wrap it:
class TransitionOrder
{
public function __construct(private WorkflowManager $wf) {}
public function handle(Order $order, string $transition): void
{
DB::transaction(function () use ($order, $transition) {
$order = Order::whereKey($order->id)->lockForUpdate()->firstOrFail();
$this->wf->apply($order, $transition); // guards run here
$order->save();
event(new OrderTransitioned($order, $transition));
});
}
}
Guards for preconditions, events for side effects. The bundle separates guards (may this transition happen?) from callbacks/hooks (what runs after?). Keep it that way: guards should be pure predicates that throw a domain exception; emails, audit rows, and notifications belong in listeners fired after the transition succeeds. Side effects inside a guard are the fastest way to get a half-applied transition when something downstream fails.
Expose allowed transitions to the UI. Call the workflow to list legal transitions for the current state and render buttons from that list. This is the single highest-value day-to-day use — it eliminates the "button that 500s" class of bug.
Test the transition matrix, not the happy path. A data provider asserting every (state, transition) pair is allowed or rejected catches ~90% of workflow regressions and takes twenty minutes to write.
symfony/workflow, which is framework-agnostic and maintained.lockForUpdate() (or an optimistic version column) before applying a transition.InvalidTransitionException and a guard that throws TypeError both surface as 500s unless you catch and distinguish. Log every transition attempt with actor, from-state, to-state, and outcome.transitions table if anyone will ever ask "who approved this and when."symfony/workflow via Composer, or a ~150-line enum + transition service, gives you the same model with maintained dependencies and no Symfony2 baggage.How can I help you explore Laravel packages today?