Configure a workflow using PHP attributes. Prefer separating the durable workflow definition from the event listener/orchestrator:
*Flowis the attribute definition class, for exampleImageFloworSubmissionFlow.*Workflowis the listener/service class that reacts to transitions, queues work, and applies app policy, for exampleImageWorkflow.
Older apps may still use *WF, *WorkflowInterface, or a single class that both defines and handles the workflow. New code should use *Flow for the definition because it is short, readable, and leaves Workflow for the runtime service.
auto-registration!
The bundle now exposes additive Twig helpers for resolving workflow definition constants without hard-coding raw place or transition strings in templates.
{% set removePlace = workflow_const(image, 'PLACE_REMOVE') %}
{% if image.marking != removePlace %}
...
{% endif %}You can also resolve by workflow name:
{% set removeTransition = workflow_const('ImageFlow', 'TRANSITION_REMOVE') %}Available helpers:
workflow_const(subjectOrWorkflow, constantName): resolves a PHP constant from the workflow definition classworkflow_name(subjectOrWorkflow): resolves the workflow name from a subject or returns the provided workflow namesurvos_workflow_metadata(workflowName, key, metadataSubject): existing metadata helper for workflow/place/transition metadata
This is additive. Existing metadata helpers and app-level Twig extensions can remain in place.
During bundle prepend/compile time, AttributesWorkflowConfigBuilder now publishes an internal map of:
workflow name => workflow definition classsupported entity class => workflow definition class[]
WorkflowHelperService uses that map to resolve the workflow definition class for either:
- a workflow name like
ImageFlow - an entity instance like
App\Entity\Image
That lets Twig resolve constants from the actual PHP workflow definition instead of relying on brittle string literals in templates.
The bundle now includes PHPUnit 13-compatible unit tests covering:
- compile-time workflow definition mapping
- constant resolution in
WorkflowHelperService - Twig helper exposure in
WorkflowExtension
Run them from the bundle root:
composer install
vendor/bin/phpunitDoctrine-free jsonl workflow: https://claude.ai/share/9c89f52c-1655-44b6-bb86-d773d29bc20b
@todo: https://joppe.dev/2024/10/11/dynamic-workflows-with-symfony-workflow-component/
for easyadmin integration, also see https://github.com/WandiParis/EasyAdminPlusBundle
<?php
// SubmissionFlow.php
namespace App\Workflow;
use App\Entity\Submission;
use Survos\StateBundle\Attribute\Place;
use Survos\StateBundle\Attribute\Transition;
use Survos\StateBundle\Attribute\Workflow;
#[Workflow(supports: [Submission::class], name: self::WORKFLOW_NAME)]
final class SubmissionFlow
{
const WORKFLOW_NAME='SubmissionFlow';
#[Place(initial: true, metadata: ['description' => "starting place after submission"])]
const PLACE_NEW='new';
#[Place(metadata: ['description' => "waiting for admin approval"])]
const PLACE_WAITING='waiting';
const PLACE_APPROVED='approved';
const PLACE_REJECTED='rejected';
const PLACE_WITHDRAWN='withdrawn';
#[Transition(from:[self::PLACE_NEW], to: self::PLACE_WAITING)]
const TRANSITION_SUBMIT='submit';
#[Transition(from:[self::PLACE_NEW], to: self::PLACE_APPROVED, guard: "is_granted('ROLE_ADMIN')")]
const TRANSITION_APPROVE='approve';
#[Transition(from:[self::PLACE_NEW], to: self::PLACE_REJECTED, guard: "is_granted('ROLE_ADMIN')")]
const TRANSITION_REJECT='reject';
#[Transition(from:[self::PLACE_NEW, self::PLACE_APPROVED], to: self::PLACE_WITHDRAWN, guard: "is_granted('ROLE_USER')")]
const TRANSITION_WITHDRAW='withdrawn';
#[Transition(from:[self::PLACE_REJECTED, self::PLACE_APPROVED], to: self::PLACE_NEW)]
const TRANSITION_RESET='reset';
}Now create a separate SubmissionWorkflow service/listener that uses these constants and acts on workflow events. The definition class stays declarative; the workflow class owns behavior.
symfony new workflow-demo --webapp --php=8.4 && cd workflow-demo
composer config extra.symfony.allow-contrib true
bin/console importmap:require d3
composer config minimum-stability beta
bin/console make:controller d3 -i
symfony server:start -d
symfony open:local --path=/d3
../survos/bin/lb.sh workflow-helper
# composer req survos/state-bundle
bin/console make:controller d3 -i
cat > templates/d3 .html.twig <<END
{% extends 'base.html.twig' %}
{% block body %}
workflow here.
{% endblock %}
END
symfony server:start -d
symfony open:local --path=/d3
Since the workflow may use a message bus, a reminder on how to configure that with the Symfony CLI: https://symfony.com/doc/current/setup/symfony_server.html#symfony-server_configuring-workers
https://github.com/survos/SurvosWorkflowHelperBundle/network/dependents https://github.com/codereviewvideos/symfony-workflow-example