Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented Oct 2, 2025

Q A
Branch? 7.4
Bug fix? no
New feature? yes
Deprecations?
Issues Fix #58503
License MIT

Hello folks!

I'm happy to share with you a new way to configure workflow. Please read the issue first.

Here is the new API:

<?php

namespace App\Workflow;

use App\Entity\Model\TaskStep;
use Symfony\Component\Workflow\Attribute\AsWorkflow;
use Symfony\Component\Workflow\Attribute\Transition;

#[AsWorkflow(
    name: 'task',
    supports: [\App\Entity\Task::class],
    // places: TaskStep::class, // Not needed, but works!
)]
class TaskWorkflow extends \Symfony\Component\Workflow\Workflow
{
    #[Transition(
        from: TaskStep::New,
        to: TaskStep::Processing,
    )]
    public const string START_PROCESS = 'start_process';

    #[Transition(
        from: TaskStep::Backlogged,
        to: TaskStep::Processing,
    )]
    public const string RETRY = 'retry';

    #[Transition(
        from: TaskStep::Processing,
        to: TaskStep::Backlogged,
    )]
    public const string TEMP_ERROR = 'temp_error';

    #[Transition(
        from: TaskStep::Processing,
        to: TaskStep::Failed,
    )]
    public const string PERMANENT_ERROR = 'permanent_error';

    #[Transition(
        from: TaskStep::Processing,
        to: TaskStep::Completed,
    )]
    public const string COMPLETE_WITHOUT_ERROR = 'complete_without_error';
}

As you can see, there is a new AsWorkflow Attribute. Thanks to it, you can configure everything about
the workflow (name, metatada, support, auditTrail, etc). As usual, places can be infered from the transitions (from and to)

Then, in the class, you can add constant to declare transitions.
It's possible to override the transition name in the attribute. (This could solve issue with many transition with the same name).

The PR is not finished yet. But I want to gather feedback first

class TestArticleWorkflow
{
#[Transition(
name: 'request_review',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is the same as the constant value, is it necessary?

);
}

$attributeReader = new AttributeReader();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is broken if the workflow component is not installed.

{
public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter('.workflow.config')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this early return is broken. This compiler pass has 2 sources of definitions. The .workflow.config parameter and services tagged as .workflow.attribute. What if you don't have the parameter while still having tagged services ?


$registryDefinition = $container->getDefinition('workflow.registry');

$config = $container->getParameter('.workflow.config');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a compiler pass defined in the component that processes a parameter storing the semantic configuration of FrameworkBundle leads to new BC guarantees we need to provide, as it means that this config is not purely internal anymore. It crosses a package boundary.
I would prefer keeping the FrameworkBundle config structure purely internal (we provide BC for the input of our Configuration classes, not for its output, as this would forbid us to make many changes we do when deprecating things)

if (!$workflow = $attributes[0]['configuration'] ?? null) {
throw new LogicException(\sprintf('The service "%s" must define the "configuration" attribute on its "%s" tag.', $id, '.workflow.attribute'));
}
$serviceId = $this->createWorkflow($container, $registryDefinition, $workflow['name'], $workflow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the name part of the configuration attribute while used separately, instead of being a separate tag attribute ?

$serviceId = $this->createWorkflow($container, $registryDefinition, $workflow['name'], $workflow);
$container
->getDefinition($serviceId)
->clearTag('.workflow.attribute')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why clearing this tag ? That definition created just above will not have this tag.

{
public function __construct(
public string $name,
public string|array $places = [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any array should have phpdoc describing the expected type of this array (otherwise, we risk having to cover array<int|string, mixed> with BC as it won't be clear what works accidentally even if it was not intentional)

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class AttributeReader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this class be @internal ?

'supports' => $attribute->supports,
'places' => $attribute->places,
'transitions' => $this->extractTransitions($reflection),
'metadata' => [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we support defining metadata in the attribute ?

continue;
}

$transition = $transitionAttributes[0]->newInstance();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should read all transition attributes, not only the first one. Otherwise, this won't allow defining multiple transitions with the same name but distinct froms in a Workflow (which is valid in the component, even though I'm not sure it is fully supported in all the dumpers)

$attributeReader = new AttributeReader();
$container->registerAttributeForAutoconfiguration(AsWorkflow::class, static function (ChildDefinition $definition, AsWorkflow $attribute, \ReflectionClass $reflection) use ($attributeReader): void {
$configuration = $attributeReader->extractConfiguration($attribute, $reflection);
$definition->addTag('.workflow.attribute', [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way to solve my objection in https://github.com/symfony/symfony/pull/61935/files#r2399400826 would be to define the attribute as AsWorkflowDefinition instead, and register .workflow.attribute as a resource tag instead, similar to how #61563 was implemented.

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be repeatable, to allow defining multiple transitions with the same name bit different from places

public ?string $name = null,
public array $metadata = [],
public ?string $guard = null,
public string|array|\BackedEnum $from = [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we support both froms and from ? This seems bad, especially when the plural argument is silently ignored when both are provided.

public function __construct(
public string|array $froms = [],
public string|array $tos = [],
public ?string $name = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we even need a name at all ? What is the use case for defining a transition name that is not equal to the constant value (making it impossible to use the constant to reference the transition) ?

}

$transition = $transitionAttributes[0]->newInstance();
$transitionName = $transition->name ?? $constant->getValue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must validate that the constant value is a string, providing proper error when using it in a wrong way instead of a weird failure (or an implicit cast leading to unclear transition name) later on.

$transition = $transitionAttributes[0]->newInstance();
$transitionName = $transition->name ?? $constant->getValue();

$transitions[$transitionName] = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexing the array by transition name forbids configuring multiple transitions with the same name but different from places, which is a valid configuration in the WorkflowDefinition (definition validators only enforce unique names among outgoing transitions of each place, not among all transitions)

@Menelion
Copy link

Menelion commented Oct 6, 2025

Hallelujah! Hope it will get merged soon, thanks @lyrixx for this work! I love the concept of workflows but hate the necessity to write that Yaml config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workflow] Declare Workflow with PHP Class and attributes

5 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.