Symfony's Workflow component models business processes as a series of places (states or steps) connected by transitions (actions that move from one place to another). An object progresses through the workflow, and its current position is tracked by the marking, which stores which place(s) the object is in.
A key feature of workflows (as opposed to state machines) is that an object can be in multiple places simultaneously. For example, when building a product, you might assemble several components in parallel. However, until now, each place could only record whether the object was there or not, like a binary flag.
Symfony 7.4 introduces multiplicity thanks to weighted transitions: a place can now track how many times an object is in that place. This is useful when you need multiple instances of something before proceeding. For example, "collect 4 legs before assembling the table" or "wait for 3 approvals before publishing".
How Weighted Transitions Work
You can assign a weight to specify how many instances are produced or required:
- Weight on output (to): the transition places the object in the target place N times;
- Weight on input (from): the transition requires the object to be in the source place N times before it can fire.
Without weights, every transition produces or consumes exactly one instance per place.
Example: Building a Table
Consider a workflow that models table construction. A table needs four legs, one tabletop, and we also want to track timing. Here's how weighted transitions handle this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
# config/packages/workflow.yaml
framework:
workflows:
make_table:
transitions:
start:
from: init
to:
- place: prepare_leg
weight: 4
- place: prepare_top
weight: 1
- place: stopwatch_running
weight: 1
build_leg:
from: prepare_leg
to: leg_created
build_top:
from: prepare_top
to: top_created
join:
from:
- place: leg_created
weight: 4
- top_created
- stopwatch_running
to: finished
When using the following code:
1 2 3 4 5 6 7 8 9 10 11
$subject = new Subject();
$workflow = new Workflow($definition);
$workflow->apply($subject, 'start');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_top');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'join');
This is how the execution flow works:
- Firing
startmarks the object as being inprepare_legfour times, inprepare_toponce, and instopwatch_runningonce; - Each time you fire
build_leg, it consumes one instance fromprepare_legand adds one toleg_created(fire it four times to accumulate four instances); - Firing
build_topcreates the single tabletop; - The
jointransition requires the object to be inleg_createdexactly four times, plus once in each of the other places, before it can fire.
This creates a synchronization point: you cannot proceed to assembly until all required components reach their specified quantities. The workflow enforces these structural constraints at the model level.