-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Workflow] Declare Workflow with PHP Class and attributes #58503
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
Comments
I like the idea of using PHP attribute but if the YAML configuration is the problem, why not declaring your workflow using the PHP syntax ? use App\Entity\Intervention;
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
$workflow = $framework->workflows()->workflows('intervention_status');
$workflow
->type('state_machine')
->supports([Intervention::class])
->initialMarking(['state_qualify']);
$workflow->markingStore()
->type('method')
->property('currentPlace');
// defining places manually is optional
$workflow->place()->name('state_qualify');
$workflow->place()->name('state_exclusions');
// ...
$workflow->transition()
->name('to_review')
->from(['state_qualify'])
->to(['state_exclusions']);
// ...
}; You will also be able to use enums instead of string. |
Yes, that's where I started, and it works well as long as it's not too complicated. As soon as you start having 3 or 4 workflows in a project, the file becomes too long, so you want to separate it into several files or call services, but you don't have services autowire because you're not in a class, so you end up making basic PHP file includes. Likewise, if you want constants (or an enum), you have to separate them in a dedicated class, and you start to end up with 3 files to declare a single workflow (the php config, the class/enum to declare states and the class/enum to declare transitions), which quickly becomes cumbersome. Hence the idea of passing this to a PHP class that contains the list of states, the list of transitions and the Workflow declaration in the same place. It's easy to read and understand for another developer, or when you come back to the project after 1 year. Other advantages include the flexibility of storing the class wherever you like, and the autowire of our Workflow class with direct access to states, transitions and workflow functions (can(), apply(), ...). Lastly, it's closer to the way other Symfony components work. For example, for forms, we don't add all our form declarations in a forms.yaml file, which quickly becomes unreadable, nor in a declarative php file. Instead, we create a dedicated class, which is clearer and more readable, and brings other advantages such as form composition, autocompletion, etc... |
this is not true. Enums are objects in PHP, and so cannot be used in places expecting strings. You will be able to use constants, though. |
I was talking about using backed enums. Ex: |
@Jean-Beru |
Thanks for the clarification on enums. To refocus, what do you think of the proposal to create AsWorkflow, Place and Transition attributes to declare workflows using PHP classes? Bearing in mind that this doesn't prevent you from using the classic yaml or php config, it's just another possibility offered to the developer. |
Up ? We tested and it looks functional. It's missing metadata but seems easy to add on the attribute. |
Hello there. I like the idea a lot ! I thought about something similar few years ago: #[AsWorkflow(
name: 'intervention_status'
places: ['a', ...']
transitions: [
new Transition('....']
]
)]
class MyWorkflow extends Workflow // The one from symfony I don't know what is the best way to configure it. Having to declare every singe constant is a bit boring I think. (I never use constant for place or transition 😅. And for thoses telling me I'm crazy: Do you use constants for route and route parameter?) And with my proposition, you can still use constant, but there is duplicated code... Anyway, thanks for bringing that up. However, if we merge that into Symfony, we'll have to rework how it works, because we must not use reflection at runtime, and all the autowiring must be keep. I need to finish to review some issue/pr, and if I have time before the end of the week, I'll try to work on an RFC with the best format |
Another idea: #[AsWorkflowDefinition(initialMarking: 'submitted')]
enum MyWorkflow: string
{
case Submitted = 'submitted';
case Ham = 'ham';
case PotentialSpam = 'potential_spam';
case Spam = 'spam';
case Rejected = 'rejected';
case Published = 'published';
public const ACCEPT = [
'from' => self::Submitted,
'to' => self::Ham,
];
public const MIGHT_BE_SPAM = [
'from' => self::Submitted,
'to' => self::PotentialSpam,
];
public const REJECT_SPAM = [
'from' => self::Submitted,
'to' => self::Spam,
];
public const PUBLISH = [
'from' => self::PotentialSpam,
'to' => self::Published,
];
public const REJECT = [
'from' => self::PotentialSpam,
'to' => self::Rejected,
];
public const PUBLISH_HAM = [
'from' => self::Ham,
'to' => self::Published,
];
public const REJECT_HAM = [
'from' => self::Ham,
'to' => self::Rejected,
];
// optional method, by default we take all consts on the enum
public static function getTransitions(): array
{
return [
self::ACCEPT,
self::MIGHT_BE_SPAM,
self::REJECT_SPAM,
self::PUBLISH,
self::REJECT,
self::PUBLISH_HAM,
self::REJECT_HAM,
];
}
} |
In a workflow, each places should be unique. But transition names could be duplicated. If you use a constant for that, you'll have a a set |
Thanks for your responses ! Putting everything in the declaration of the attribute and nothing in the class is like making a yaml config file, you don't have autocompletion, you don't have the syntax of the classes, etc. Why not an enum for the list of states? Why not an enum for the list of states, but transitions declared as const of type array is also a bit strange. It means that if you have the AsWorkflow attribute then all constants containing an array are necessarily transition definitions, you lose the ability to add other array constants if you need them and unless you use a validation system when loading the class you can't be sure that the “to” and “from” entries are properly formatted. The “Transition” attribute clearly defines the intention, provides the flexibility of setting it to a constant or not, and allows you to type the parameters. If we take the example of live components, each LiveProp is a property of the class with the #[LiveProp] attribute, and this works quite well. Can we do the same thing and use properties instead of constants? |
To continue @nicolas-grekas idea, we can attach a #[AsWorkflow(initialMarking: self::Submitted)]
enum MyWorkflow: string
{
#[Transition(to: self::Ham, name: 'accept')]
#[Transition(to: self::PotentialSpam, name: self::MIGHT_BE_SPAM)]
#[Transition(to: self::Spam, name: self::REJECT)]
case Submitted = 'submitted';
#[Transition(to: self::Published, name: 'publish')]
#[Transition(to: self::Rejected, name: 'reject')]
case Ham = 'ham';
#[Transition(to: self::Published, name: 'publish')]
#[Transition(to: self::Rejected, name: 'reject')]
case PotentialSpam = 'potential_spam';
case Spam = 'spam';
case Rejected = 'rejected';
case Published = 'published';
// Using const for transition names is very optional
public const ACCEPT = 'accept';
public const MIGHT_BE_SPAM = 'might_be_spam';
public const PUBLISH = 'publish';
public const REJECT = 'reject';
} And I would make it optional to use a backed enum: the case name can be used for concision. If a different string representation is later required, they can switch to backed enum without breaking the existing names. The transition name can also be optional. You get |
I like this proposal! When registering each Transition attribute, maybe it's possible to declare properties or constants on the fly to have autocompletion for Transitions without having to declare additional constants by hand because it's a bit duplicative? I could spend some time testing this but I don't know enough about how Symfony works at runtime, @lyrixx said ‘we must not use reflection at runtime’, I don't know how else to do it. |
The configuration needs to be read during the compilation of the container. Then create a CompilerPass that finds all the definitions for this tag, create the workflow configuration and inject it like this: symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php Lines 1145 to 1163 in ed7dba6
|
Thanks, I'll try that this week. |
the API proposed by @GromNaN works for state machines where each transition has a single place for |
Hi, I've tried various things, but none of them have really convinced me. The problem isn't really “how to improve the definition of a workflow”, it's more “how to improve the use of a workflow”. My original proposal was:
=> It seems that your comments stopped there. And indeed, in this case, an enum and just a Transition attribute like @GromNaN proposal are more appropriate. (@stof and if it covers both state machine / workflow cases because the same transition, i.e. with the same name, can be declared twice, on two different statuses and/or on the same status but with two different “to”). But the proposal went further than that:
So back to the original idea. Here's an example of a complete workflow:
|
Description
Hello,
I really like the Workflow component, but not his yaml configuration. Constants in yaml are too verbose, as is passing config to php format. So I tried to create three attributes: [AsWorkflow], to be set on a class (which should extend AbstractWorkflow), [Place], to be set on a constant, and [Transition], also to be set on a constant. With these three attributes, and a few configurations that I've put in a dedicated bundle for now, it's possible to add to Symfony's Workflow component the ability to simply declare a Workflow via a PHP class, autowire it directly from that class (you can use can(), apply(), .. functions directly from the class) and, if need be, mix it with the classic yaml configuration without any hassle or BC Break.
In the bundle, metadata management, testing and documentation still need to be completed, but the code works well, it's being used in real projects and everything's running smoothly. Before going any further, I'd like to hear your opinion on whether this could be integrated directly into the Workflow component? Is this the right approach? I could do a PR if I get positive feedback.
The code is available here => https://packagist.org/packages/letots/workflow-extension-bundle
To test, simply run the compose require letots/workflow-extension-bundle command in a recent Symfony project (attribute support is required) and create a new class (see example below). You can then debug the workflow, use it in any other class with autowire (you can still use the WorkflowInterface with Target attribute but you can also inject directly your class), ...
Is it really useful? Apart from the syntax, which I personally find clearer in PHP, we have the advantage of not having to configure anything for this to work, of having a separate PHP file for each Workflow and therefore of having autowiring with autocompletion of the class name, the advantages of using constants for autocompletion and validation to define the names of Places and Transitions (no need to go back 10 times in the workflow.yaml file to find out how a particular status was named), ... Last argument, even if this only applies to a few very specific cases, it is possible to create a dynamic workflow by rewriting the getPlaces() and getTransitions() functions of the AbstractWorkflow file, in order to add Places dynamically, for example, depending on the properties of the target object, all grouped together in the dedicated workflow class.
Example
The text was updated successfully, but these errors were encountered: