-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[Workflow] Allow to define workflow with PHP attributes #61935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.4
Are you sure you want to change the base?
Conversation
class TestArticleWorkflow | ||
{ | ||
#[Transition( | ||
name: 'request_review', |
There was a problem hiding this comment.
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?
src/Symfony/Component/Workflow/Tests/Configuration/AttributeReaderTest.php
Show resolved
Hide resolved
); | ||
} | ||
|
||
$attributeReader = new AttributeReader(); |
There was a problem hiding this comment.
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')) { |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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 = [], |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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' => [], |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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', [ |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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 = [], |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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] = [ |
There was a problem hiding this comment.
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)
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. |
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:
As you can see, there is a new
AsWorkflow
Attribute. Thanks to it, you can configure everything aboutthe 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