Symfony 8.2 New Published on

Describing a workflow with attributes

#[AsWorkflow] and #[Transition]: the state machine, its transitions and its guards fit in a single class instead of a YAML file.

Topics covered: WorkflowGuards

New in Symfony 8.2 (in development). A workflow used to be described in YAML: its places, its transitions, the object it follows. Its guards and listeners lived elsewhere, in classes that referred to it by name, and the code that moved it along repeated the transition names as strings. Symfony 8.2 brings it all together in one class.

Before

framework:
    workflows:
        ticket:
            type: state_machine
            supports: [App\Model\Ticket]
            marking_store: { type: method, property: marking }
            initial_marking: open
            transitions:
                start: { from: open, to: in_progress }
                # …
public function history(#[Target('ticket')] WorkflowInterface $workflow): Response
{
    $workflow->apply($ticket, 'start');

Since Symfony 8.2

#[AsWorkflow(supports: Ticket::class)]
final class TicketWorkflow
{
    use WorkflowTrait;

    #[Transition(from: 'open', to: 'in_progress')]
    public const START = 'start';

    #[AsGuardListener(transition: self::START)]
    public function check(GuardEvent $event): void { /* … */ }
}
public function history(TicketWorkflow $workflow): Response
{
    $workflow->apply($ticket, TicketWorkflow::START);

#[AsWorkflow] turns the class into a workflow, named after it: TicketWorkflow gives ticket. It is a state machine by default (type: WorkflowType::Workflow makes it a workflow, where an object can be in several places at once), and the marking is stored in the subject's marking property. Each constant marked #[Transition] is a transition; the places are inferred from them, and the input place (from) of the first transition becomes the initial place. An enum can also provide the places, with places: and #[Place] for their metadata.

The class remains an ordinary service: it receives its dependencies and carries its own listeners, which no longer need to name the workflow. With WorkflowTrait, it also exposes the methods of a workflow: you inject it, and its constants replace the strings.

Your task

Customer support tracks its tickets with a state machine described in config/packages/workflow.yaml, and its guard, which forbids resolving a ticket without an answer, already lives in TicketWorkflow. The /ticket preview walks through a ticket's lifecycle and shows, at each step, the enabled transitions.

  1. Describe the ticket workflow in TicketWorkflow, one constant per transition, and remove it from the YAML.
  2. Let the guard apply to the workflow of its class, without naming it.
  3. In TicketController, receive TicketWorkflow and replace the transition names with its constants.

The preview must show exactly the same path as before.

Where the feature comes from: the original pull request.