Skip to content

[Workflow] make $registry->get(Entity::class) consistent with the doctrine way (by using class name) #37883

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

Closed
wants to merge 8 commits into from

Conversation

epitre
Copy link
Contributor

@epitre epitre commented Aug 19, 2020

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? no
Tickets No
License MIT
Doc PR symfony/symfony-docs#14106

For this, I had to change some Interface, but I don't think it causes BC Breaks

Now that I'm submitting it, I wonder if it's been submitted before 🤔

Anyway, I think it's more consistent to use the class name to retrieve a workflow.

It's particularly convenient when you need the metadata of the workflow. In that case, you might not have an instance available and you have to do something like $registry->get(new Entity()).

With this PR, $registry->get(Entity::class) will work :)

@epitre
Copy link
Contributor Author

epitre commented Aug 19, 2020

The failed tests do not seem to be related to workflow ^^

Copy link
Member

@lyrixx lyrixx left a comment

Choose a reason for hiding this comment

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

Good idea 👍🏼
I left some minor comment

@@ -18,5 +18,5 @@
*/
interface WorkflowSupportStrategyInterface
{
public function supports(WorkflowInterface $workflow, object $subject): bool;
public function supports(WorkflowInterface $workflow, $subject): bool;
Copy link
Member

Choose a reason for hiding this comment

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

I don't know if it's a BC break / cc @fabpot or @nicolas-grekas

Copy link
Member

Choose a reason for hiding this comment

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

It is a BC break, so it's not allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think I can do what I want without all these BC Breaks :/

So, shall we move it to Symfony6 or it's not worth it ?

Copy link
Member

Choose a reason for hiding this comment

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

We never break BC without an upgrade path. So, Symfony 6 won’t help.

Copy link
Contributor

Choose a reason for hiding this comment

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

This PR brings a good change in my opinion. Providing an upgrade path for those kind of changes was easier in the bad old days without typing.
Is there another good or even better name for the interface to deprecated this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a stupid question that I can't answer.
I'm sorry to bother you with this.

How loosening the typing makes it a BC break ?

I tried this code :

<?php
class A {
	function f($arg) {
		var_dump($arg);
	}
}

class B extends A {
	function f(object $arg) {
		var_dump($arg);
	}
}

$a = new A();
$a->f((new \stdClass));

$a = new A();
$a->f(1);

$b = new B();
$b->f((new \stdClass));

But everything is working as expected.
What am I missing ?

Copy link
Contributor

Choose a reason for hiding this comment

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

The declaration of B::f violates LSP since A states that $arg accept anything, restricting it to only objects in B means that an instance of B can't be used as an instance of A.

I'm not sure why you didn't get an error, your code should fail with the following error:

Declaration of B::f(object $arg) must be compatible with A::f($arg)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed
if I add declare(strict_types=1); I have the error.

Thank you @jvasseur

epitre and others added 4 commits August 19, 2020 13:51
Co-authored-by: Grégoire Pineau <lyrixx@lyrixx.info>
Co-authored-by: Grégoire Pineau <lyrixx@lyrixx.info>
@@ -18,5 +18,5 @@
*/
interface WorkflowSupportStrategyInterface
{
public function supports(WorkflowInterface $workflow, object $subject): bool;
public function supports(WorkflowInterface $workflow, $subject): bool;
Copy link
Member

Choose a reason for hiding this comment

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

It is a BC break, so it's not allowed.

@@ -41,7 +41,7 @@ public function has(object $subject, string $workflowName = null): bool
/**
* @return Workflow
*/
public function get(object $subject, string $workflowName = null)
public function get($subject, string $workflowName = null)
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 a BC break as the class is not final.

Copy link
Member

Choose a reason for hiding this comment

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

Still a BC break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oups, I deleted my comment unintentionally.

Anyway, I'm new to this. I don't understand what kind of path you want me to take.

Should I, for example :

  1. create another method
  2. deprecate this one

And that case, how can I have the consistency I am looking for (which only goes through this BC Breaks) ?

@@ -29,9 +30,17 @@ public function __construct(string $className)
/**
* {@inheritdoc}
*/
public function supports(WorkflowInterface $workflow, object $subject): bool
public function supports(WorkflowInterface $workflow, $subject): bool
Copy link
Member

Choose a reason for hiding this comment

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

This one is not a BC break as the class is final.

Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

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

this seems only useful for InstanceOfSupportStrategy, not sure we should make this design change.

@ro0NL
Copy link
Contributor

ro0NL commented Aug 19, 2020

alternatively, we could consider Registry::getForClass(string $class), checking the supported instanceof strategies (based on its getClassName()) manually

otherwise, i'd argue the usefulness of strategies in general (e.g in favor of mapping by class by design)

@lyrixx
Copy link
Member

lyrixx commented Aug 19, 2020

otherwise, i'd argue the usefulness of strategies in general (e.g in favor of mapping by class by design)

I agree, but this was asked in an issue / pr

@ro0NL
Copy link
Contributor

ro0NL commented Aug 19, 2020

wait, to solve this, isnt it simply matter of adding Registry::getByName(string $workflowName)?

assuming your definitions are "class named" (that's our convention at least). That seems like a much cleaner and explicit approach to me. (To me getting a random workflow, based on "strategies" always seemed like a WTF :))

@@ -41,7 +41,7 @@ public function has(object $subject, string $workflowName = null): bool
/**
* @return Workflow
*/
public function get(object $subject, string $workflowName = null)
public function get($subject, string $workflowName = null)
Copy link
Member

Choose a reason for hiding this comment

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

Still a BC break.

@@ -31,6 +32,10 @@ public function __construct(string $className)
*/
public function supports(WorkflowInterface $workflow, object $subject): bool
{
if ($subject instanceof \stdClass && property_exists($subject, 'class')) {
Copy link
Member

Choose a reason for hiding this comment

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

Is it really worth it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

consistency is part of a good programming. Unfortunately, it is not PHP's strong suit.

I'm not a big fan of this.

Might not be worth it, indeed.

@epitre
Copy link
Contributor Author

epitre commented Aug 20, 2020

@lyrixx @ro0NL @simonberger @fabpot
So now, I'm thinking of 3 different path I can take :
1️⃣ I just give up on this consistency dream
2️⃣ I create a new method called getByName in Registry and maybe (or maybe not ?) deprecate get
3️⃣ I create another service called WorkflowRegistry and deprecate Registry

What do you prefer ? Or do you have a 4️⃣ option ?

@ro0NL
Copy link
Contributor

ro0NL commented Aug 20, 2020

it should be clear subject (an object) and workflow name (a string) are different things

get and as such strategies expect the subject; the subject is always an object instance. IMHO this shouldnt change (hence i 👎 the PR currently)

a new getByName could return the workflow by name (thus not by subject)

then, if you use "class named" workflows, Registry::getByName(Some::class) will work

@fabpot
Copy link
Member

fabpot commented Aug 20, 2020

I wouldn't change anything. But @lyrixx has the last word.

@simonberger
Copy link
Contributor

@ro0NL The workflow name does not have to be unique (among different workflows), right? Not absolutely a showstopper for a getByName method but throws an exception in some scenarios.

@fabpot fabpot added this to the next milestone Aug 23, 2020
@lyrixx
Copy link
Member

lyrixx commented Aug 24, 2020

The Registry::get(object $subjet) is mandatory. We can not remove / deprecated it. It useful in twig for example.

Then, you should not forget that a subject (doctrine entity or anything else) can have multiple workflows on it.
For example one for the "billing + shipping" and another one for claim.

So getting the workflow by name should not be encouraged.

But your usecase is valid so I'm bit embarrassed by not having a nice solution :/. But I like the @ro0NL idea a lot. We can index all workflows by name (and not by subject FQCN). But you can use the FQCN for the workflow name :)

Do you want to give a try to?

@epitre
Copy link
Contributor Author

epitre commented Aug 25, 2020

@lyrixx ok, I'll see what I can do.

@lyrixx
Copy link
Member

lyrixx commented Aug 28, 2020

Actually, I don't think this is a good idea: An object can have many workflow, so getting the workflow by its name does not make sens.

Then, someone has the same need than you and I think #37979 is a simpler / better solution.

WDYT?

@epitre
Copy link
Contributor Author

epitre commented Aug 28, 2020

@lyrixx

I did not use Definition before so I'm not really familiar with it.

If I understand correctly, Definition is specific to a workflow.
And are you suggesting to go through Definition to get to the Metadata 🤔 ?

How do you inject a specific Definition ? With services.yaml ? Is it possible to inject a specific Workflow ?

I think I don't understand how it helps me. Maybe a PR in symfony-docs might help me :D

And I also have another use case, totally different (and maybe not relevant)
Here is an extract from my code :

$workflow = $this->workflowRegistry->get(new Order());

foreach ($orders as $order) {
    $transitions = $workflow->getEnabledTransitions($order);
    [...]
}

I feel that something more intuitive would be :

$workflow = $this->workflowRegistry->get(Order::class);

foreach ($orders as $order) {
    $transitions = $workflow->getEnabledTransitions($order);
    [...]
}

I can also do something like

$workflow = $this->workflowRegistry->getByName(Order::class);

foreach ($orders as $order) {
    $transitions = $workflow->getEnabledTransitions($order);
    [...]
}

^^

But I probably missed your point, sorry :/ Do you have an example ?

@lyrixx
Copy link
Member

lyrixx commented Aug 28, 2020

First, you don't need to use the registry to get a workflow. For the record, the Registry has been implemented to be able to get a workflow in twig template. In pure PHP code, I recommend you to inject directly the workflow instance that match your entity.

If you have many workflows, you can use typed + named parameter. For example, with the following configuration, the following services are available:

 Symfony\Component\Workflow\WorkflowInterface $andWorkflow (workflow.and)
 
 Symfony\Component\Workflow\WorkflowInterface $articleWorkflow (workflow.article)
 
 Symfony\Component\Workflow\WorkflowInterface $orWorkflow (workflow.or)
 
 Symfony\Component\Workflow\WorkflowInterface $roundTripWorkflow (workflow.round_trip)
 
 Symfony\Component\Workflow\WorkflowInterface $straightWorkflow (workflow.straight)
 
 Symfony\Component\Workflow\WorkflowInterface $taskStateMachine (state_machine.task)
 
 Symfony\Component\Workflow\WorkflowInterface $wtfWorkflow (workflow.wtf)

This means, I'm able to inject a workflow like this: https://github.com/lyrixx/SFLive-Paris2016-Workflow/blob/master/src/Controller/ArticleController.php#L54 or like this:

class A 
{
    public function __contruct(WorkflowInterface $articleWorkflow)
}

I found the list of available service with the following command:

 bin/console  debug:autowiring workflow

With that in mind, You don't need the registry anymore, and so I think we can close the PR.


A note about the definition (the class that holds the workflow configuration (place + transition + metadata store) and the metadata store (the class that holds all metadata) : You can inject theme in any class. But they are not autowireable.

Your need the following code:

class A 
{
    public function __contruct(Definition $definition)
}
# services.yaml
services:
    App\A:
    parameters:
        $definition: @workflow.article.definition

Note: I found the service to inject thanks to the following command

bin/console  debug:container definition

I hope I solved all your issues and question.

If you learnt new things, may I ask you to open a PR to the doc to explain to others what you learnt ? 🙏🏼 Thanks a lot

@epitre
Copy link
Contributor Author

epitre commented Aug 28, 2020

@lyrixx
Thank you very much.
I did not know about the typed + named parameter and I did not think about checking bin/console debug:autowiring workflow.

I will update my PR-docs because the injection of Registry is the recommended way in the docs and AFAIK there's no mention of typed + named parameter injection.

@lyrixx
Copy link
Member

lyrixx commented Aug 28, 2020

Thanks a lot for your efforts. I'm closing this pr. But if you have others questions don't hesitate to ask here

@lyrixx lyrixx closed this Aug 28, 2020
@nicolas-grekas nicolas-grekas modified the milestones: next, 5.2 Oct 5, 2020
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.

8 participants