Search by

survos / workflow-extras-bundle

tacman1123

Metadata conveniences and next-transition selection for native Symfony workflows

Package info

github.com/survos/workflow-extras-bundle

Type:symfony-bundle

pkg:composer/survos/workflow-extras-bundle

Fund package maintenance!

kbond

Statistics

Installs: 43

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

2.42.13 2026-10-14 20:31 UTC

This package is auto-updated.

Last update: 2026-10-14 20:32:17 UTC


README

Small conveniences on top of native Symfony 8.2 workflow attributes. Symfony continues to discover and register the workflow, validate its definition and execute its transitions. There is no Survos definition reader or registrar here.

Experimental 0.1 scaffold, requiring PHP 8.5 and 8.2.*@dev integration packages. This is not a drop-in replacement for state-bundle yet.

Attributes

use Survos\WorkflowExtrasBundle\Attribute\AsWorkflow;
use Survos\WorkflowExtrasBundle\Attribute\Place;
use Survos\WorkflowExtrasBundle\Attribute\Transition;

#[AsWorkflow(name: 'packages', supports: Package::class, description: 'Enrich packages')]
final class PackageWorkflow
{
    #[Place('Package discovered', initial: true, next: [self::LOAD])]
    public const DISCOVERED = 'discovered';

    #[Place('Package metadata loaded')]
    public const LOADED = 'loaded';

    #[Transition(
        self::DISCOVERED,
        self::LOADED,
        'Fetch package metadata',
        label: 'Load',
    )]
    public const LOAD = 'load';
}

These classes extend Symfony's corresponding attributes, enabled by merged Symfony #66687. Place(initial: true) is native through #66722, and description on AsWorkflow, Place and Transition is native through #66872: extras passes it to the native attribute and stores nothing itself. Both transition endpoints reference declared place constants, so each stored place name is defined once. We explicitly declare and describe every place and transition instead of relying on inferred places, making the definition useful to humans and tools. Native backed enum cases and Arc arguments remain usable; enums are optional.

Attribute Arguments
AsWorkflow Identical to native (including description); kept for future workflow-level extras.
Place description, label, next, initial, metadata, icon, iconClass.
Transition from, to, description, label, next, guard, metadata, icon, iconClass.

These convenience signatures intentionally differ from native positional arguments. Use named arguments after the description when migrating existing declarations. label and next become native metadata. next accepts a transition name or an ordered list, normalized to a list; null omits it and an empty list stays empty. Metadata-only next values are normalized too. Names must be non-empty strings; associative lists and conflicting explicit/metadata values are rejected. A matching label or normalized next value in both locations is accepted. Other metadata is preserved. Native description conflict rules still apply. Native parent properties remain readonly, and Transition remains repeatable. Do not apply both a native and derived Place/AsWorkflow to the same target.

With the extras bundle enabled, missing labels are filled during container compilation from the PHP enum case or constant name: Discovered = 'new' displays "Discovered", and FETCH_DOCS = 'fetch_docs' displays "Fetch docs". CamelCase and underscores are humanized to sentence case; legacy PLACE_ / TRANSITION_ prefixes are omitted. Explicit labels (including metadata labels) win. The generated label is ordinary metadata, shared by native dumpers and the explorer. Direct attribute construction without container compilation cannot infer its declaration name. Stored identifiers remain unchanged.

Place attributes can be applied to string-backed enum cases; pass the enum class as AsWorkflow(places: PackagePlace::class) and use cases directly as transition endpoints. Place labels, descriptions, initial flags and next candidates are read by Symfony's native attribute reader.

Native descriptions came from #66726, implemented in #66872 and made description-first in #66890. Core's dumpers already read them (workflow:dump --with-metadata, PlantUML), as does this bundle's optional explorer template.

Next selection and synchronous continuation

$next = $resolver->firstEnabled($workflow, $subject, ['validate', 'reject']);
if ($next !== null) {
    // Apply synchronously, or persist/commit and explicitly dispatch through async.
}

Inject Survos\WorkflowExtrasBundle\Service\NextTransitionResolver. It validates the entire candidate list, then selects the first transition enabled by native guards. It does not apply, enqueue, flush or commit anything. Native can() can initialize marking and run guard listeners, so selection is not guaranteed to be side-effect-free. A shared completed-transition listener and next runner select the first enabled next candidate automatically. Extras uses DirectTransitionExecutor to call native apply(); workflow-messenger replaces that executor with message dispatch. Transition next metadata takes precedence over the active places; an explicit empty list stops. cascade: none suppresses chaining, and repeated marking/transition pairs raise a cycle error. The listener never flushes or dispatches messages.

An opted-in persistence adapter takes precedence over immediate chaining. The Messenger executor requires saved state: its handler defers continuation during native apply, saves the subject, then invokes the same next runner. Direct native apply callers using Messenger need a persistence adapter (such as the optional Doctrine post-flush integration); do not expect completed events to dispatch before storage is safe. SubjectStoreInterface now provides reference(), load() and save(). Extras alone needs none of these Messenger contracts.

Installation

After publication, in an application whose Symfony constraints allow 8.2-dev:

composer config minimum-stability dev
composer config prefer-stable true
composer require 'survos/workflow-extras-bundle:dev-main' --with-all-dependencies

Review and lock the resolved versions. Enable Survos\WorkflowExtrasBundle\SurvosWorkflowExtrasBundle if Flex has not done so. It requires WorkflowBundle and Survos Kit, not FrameworkBundle, Doctrine ORM, Twig or Messenger. Kit itself requires PHP 8.5 and HttpKernel.

Extras owns common attributes and next selection. It does not declare typed async or transport arguments. For queued paths use Survos\WorkflowMessengerBundle\Attribute\Transition (often aliased as AsyncTransition); that attribute extends extras, retaining every common argument in the same position and appending async and transport. Async requires extras; extras remains usable without Messenger or async. Native metadata remains an interoperability option. A definition with async: true but no enabled async runtime fails compilation with an installation/configuration hint.

Current scope and roadmap

Capability Status
Derived AsWorkflow/Place/Transition and metadata conveniences Implemented
Descriptions (#66872) Native, description-first (#66890)
Ordered first-enabled transition selection Implemented
Synchronous next chaining without Messenger Implemented; persistence adapters can defer it
state:iterate / state:stats for Doctrine entities Implemented (optional, with DoctrineBundle)
Explorer template, graph controller and grouped transition paths Implemented optional integration
State-bundle migration and deprecation After validated replacements and migration documentation

The first example is the packages workflow: metadata and README fetching on separate queues, with ordinary validation between them. Start with the example in mono. Its test suite also runs this bundle's tests. The next application checkpoints are searchbench and harvest. The packages application now uses both bundles without state-bundle, and without workflow commands, controllers or adapters of its own.

From a standalone bundle checkout, composer install followed by composer test runs its focused attribute/selector tests. The mono example additionally tests native discovery and real Messenger execution with both bundles enabled.

Workflow catalog and optional explorer

Inject Survos\WorkflowExtrasBundle\Service\WorkflowCatalog for tooling that needs workflow declarations without constructing an entity. all() returns names with declared supports, support_strategy, source_class and type; forClass() returns all matching names, including interface matches; get() retrieves the native workflow service. Runtime eligibility remains Symfony Registry's responsibility, especially for custom support strategies.

The catalog copies the native workflow service tag attributes added by Symfony #66926 (8.2). source_class is null for configuration-defined workflows; a workflow with a support_strategy declares no supports. Default labels apply to every workflow with a source_class, so plain native #[AsWorkflow] classes get them too. Use a symfony/workflow 8.2 revision that contains #66926.

The explorer is routed at /workflow/{flowCode} (route survos_workflow_extras) when Twig is installed, using the @SurvosWorkflowExtras/workflow.html.twig template and the survos--workflow-extras-bundle--workflow Stimulus controller. It is read-only. Move or hide it with route_prefix and routes_enabled: false; access control stays with the application's firewall. Use TransitionGroups::forWorkflow() for its transition sidebar: repeated names with identical metadata share a heading while each path remains explicit. Different guards stay separate, and multi-place requirements are not merged. WorkflowGraphvizDumper draws state machines; use Symfony's native GraphvizDumper for workflows with multiple simultaneous places.

Optional icons

Place and Transition accept icon (for example tabler:circle-check) and iconClass (for example text-success). These become native metadata, including when inherited by async transitions. Conflicting explicit and metadata values are rejected. Async-only arguments follow all extras arguments; prefer named arguments when adopting new options.

With Symfony UX Icons enabled, the explorer renders these beside place and transition labels. Without UX Icons it keeps the labels and omits icons. Twig consumers can use workflow_icon(icon, iconClass) for consistent rendering. The async execution indicator remains separate. No inline iconStyle is provided.

Doctrine commands

With DoctrineBundle and Console installed, for any entity a workflow supports (short name or class; --workflow-name when several workflows support it):

bin/console state:stats Package                       # rows per place, with outgoing transitions
bin/console state:iterate Package -m discovered       # each row's first enabled `next`
bin/console state:iterate Package -m documented -t load --filter='vendor=symfony' --limit=10
bin/console state:iterate Package -m discovered --sync  # handle everything in this process

Transitions go through the configured executor: applied (and flushed) directly with extras alone, queued with workflow-messenger. --sync wraps the run in TransitionExecutorInterface::synchronously(), so continuations and kickoffs of entities created along the way are handled in-process too. --cascade=none runs only the selected transition. A state machine's place is filtered in SQL through its marking property; other workflows are read per entity.

Optional Doctrine continuation

With DoctrineBundle installed, WorkflowKickoffListener needs no application code. Before insert, an entity whose marking is empty (null or uninitialized) gets its workflow's initial place(s) from native getMarking(), so constructors need not set it: declare public ?MyPlace $marking = null;. After a flush, an entity persisted in its workflow's initial place runs the first enabled next of that place; an entity imported in a later place is left alone. Workflows are found by their declared supports. Without Messenger, next transitions apply immediately and the listener flushes the result. With workflow-messenger-bundle, a transition applied directly by application code continues once its entity is flushed, and queued requests continue in the worker. Completion events are drained only by the entity manager managing the subject. Nested flushes are guarded, and worker reset clears pending work.

A flush inside an active outer transaction is rejected: use an outbox for those producers. Post-flush dispatch alone is not atomic with the database commit.