Grégoire Pineau
Contributed by Grégoire Pineau in #60201

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:

  1. Firing start marks the object as being in prepare_leg four times, in prepare_top once, and in stopwatch_running once;
  2. Each time you fire build_leg, it consumes one instance from prepare_leg and adds one to leg_created (fire it four times to accumulate four instances);
  3. Firing build_top creates the single tabletop;
  4. The join transition requires the object to be in leg_created exactly 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.

Published in #Living on the edge