Skip to content

Navigation Menu

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

[Workflow] Add transition blockers #26076

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

Merged
merged 2 commits into from
Mar 20, 2018
Merged

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented Feb 7, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #24745 #24501
License MIT

@@ -79,25 +81,28 @@ public function getMarking($subject)
/**
* {@inheritdoc}
*/
public function can($subject, $transitionName)
public function can($subject, $transitionName, bool $throwException = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Imho $throwException should be default true on next major. A deprecation should be added for calling ->can with $throwException = false. keeping this bool around for longer than necessary is bloating the API imho.

Copy link
Member Author

Choose a reason for hiding this comment

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

I disagree with that ;) IMHO knowing exactly why a transition is blocked is not so common.
more over, in twig it will not work well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. Imho Every user driven workflow wants to know exactly why a transition isnt enabled. And also what transitions can be enabled in a given state (transition availibility). For highly automated workflows i can See your point.

To Clarify my desired behavior: can s‭hould always Return a bool except when a transition isnt even defined in the workflow. A second method should always return a blocker list (except for undefined transitions). A third method isTransitionDefined That always returns bool and Never throws.

Copy link
Member Author

Choose a reason for hiding this comment

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

To Clarify my desired behavior: can s‭hould always Return a bool except when a transition isnt even defined in the workflow. A second method should always return a blocker list (except for undefined transitions). A third method isTransitionDefined That always returns bool and Never throws.

Why not ;) What do other think ?

Copy link
Member Author

Choose a reason for hiding this comment

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

But I have two concerns with your proposal:

: can s‭hould always Return a bool except when a transition isnt even defined in the workflow

This is a BC break

A second method should always return a blocker list (except for undefined transitions)

What would be the name

Copy link
Contributor

Choose a reason for hiding this comment

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

A) keep the throwException param with default false, But deprecate Passing false

B) Workflow::whyNot ::checkTransition ::testTransition ::determineTransitionAvailibility (Probably the 2nd is my favorite)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, I will do that with checkTransition (I'm not really fan of this name though )

Copy link
Contributor

Choose a reason for hiding this comment

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

If you think another name is better then go for it. I myself am not convinced 100% by checkTransition

Copy link
Member Author

Choose a reason for hiding this comment

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

I did not added isTransitionDefined. IMHO it's useless. If you really think it's useful please open an issue and let's discuss it there.

I would like to merge this PR ASAP ;)

@@ -185,32 +190,41 @@ public function getMarkingStore()
return $this->markingStore;
}

private function doCan($subject, Marking $marking, Transition $transition)
private function isTransitionEnabled($subject, Marking $marking, Transition $transition, $throwException = false): bool
Copy link
Contributor

Choose a reason for hiding this comment

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

exceptions are not necessary imho. always returing a (possibly empty) TransitionBlockerList ist much more straight forward. in the calling place you can do $this->isTransitionEnabled(...)->isEnabled(). isEnabled is then a new helper method on TransitionBlockerList. maybe renaming the method to determineTransitionAvailablity or computeTransitionState clarifies the intention: $this->computeTransitionState(...)->isEnabled(). Another helper method isAvailable could be added to TransitionBlockerList, that returns true, either if isEnabled() returns true or if all blockers on the list are of type BLOCKED_BY_MARKING

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right. I will remove the throwException parameter.
But I would like to avoid the word "state" as it could be confusing (place / state / marking)
About isAvailable is don't think is really useful. We will see

@nicolas-grekas nicolas-grekas added this to the 4.1 milestone Feb 11, 2018
@lyrixx lyrixx force-pushed the workflow-blocker branch 2 times, most recently from fca2db5 to 199032a Compare February 13, 2018 13:09
*/
public function can($subject, $transitionName);
public function can($subject, $transitionName, bool $throwException = false);
Copy link
Member

Choose a reason for hiding this comment

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

Adding a new argument in the interface is a BC break, and cannot be done before 5.0

Copy link
Member Author

@lyrixx lyrixx Feb 15, 2018

Choose a reason for hiding this comment

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

The interface is not released yet. So there are no BC break ;)

Copy link
Member

Choose a reason for hiding this comment

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

then OK.
It thought it was already there in 4.0

@lyrixx
Copy link
Member Author

lyrixx commented Feb 15, 2018

This PR is ready

@lyrixx lyrixx force-pushed the workflow-blocker branch 2 times, most recently from 5d364bd to c6a786d Compare February 16, 2018 09:48
@lyrixx
Copy link
Member Author

lyrixx commented Feb 16, 2018

I would like to merge this PR today (because it's open for a week now), so I'm calling for a review :) Thanks
(cc / @stof @nicolas-grekas @Nyholm @xabbuh )

*/
final class TransitionBlocker
{
const BLOCKED_BY_MARKING = '19beefc8-6b1e-4716-9d07-a39bd6d16e34';
Copy link
Member

Choose a reason for hiding this comment

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

What are these?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was in the initial PR.

It's used to identify a blocker by a "machine". It's the same approach as in the Validator component.

Copy link
Member

Choose a reason for hiding this comment

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

I would make them public explicitly

@d-ph
Copy link

d-ph commented Feb 18, 2018

Hello,

Sorry to muddle the PR even further, but there's a functionality regression in this PR that the original PR (although including an ugly sounding private method name: getEnabledTransitionsByNameOrTransitionBlockerList()) didn't have.

Namely, my PR was optimised for not having to run the guards twice and still being able to tell what the transition blockers were, if they occurred.

For example, imagine the following code:

try {
    $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 'foo-transition');

    if (!$transitionBlockerList->isEmtpy()) {
        // handle this exceptional case by e.g. showing an error in the UI.
        return;
    }

    $workflow->apply($subject, 'foo-transition');
} catch (UndefinedTransitionException $exception) {
    // handle the exception and/or rethrow.

    throw $exception;
}

In the code above, the transition guards are run twice -- first for buildTransitionBlockerList() and second for apply(). This is not only not ideal but also potentially seriously time consuming. Imagine one of the guards are doing an API call to a 3rd party system for some reason. The API call would need to be done twice because of the way the Workflow code is written.

As I mentioned, the original PR optimised for this scenario by allowing devs to do the following:

try {
    $workflow->apply($subject, 'foo-transition');
} catch (BlockedTransitionException $exception) {
    // handle this exceptional case by e.g. showing an error in the UI.

    return;
} catch (UndefinedTransitionException $exception) {
    // handle the exception and/or rethrow.

    throw $exception;
}

In the code above, devs just call the 'apply()' method and let if fail, if the transition is blocked. If the devs are interested in the exact reason why, then they can catch the BlockedTransitionException exception and retrieve the TransitionBlockers that blocked the transition. Most importantly though, the guards are not called twice, which was one of the two reasons I created the original PR in the first place (the first reason was to introduce the TransitionBlockers). Maybe it'd be a good idea to further require this behaviour by writing a unit test for it (testGuardsAreCalledOnceWhenApplyIsCalledAndTransitionBlockerListIsStillKnown()).

Although I appreciate the work put in this PR, the fact that guards are called twice doesn't make me very happy as a consumer of the Workflow component.

Could something be done in this PR to make sure the guards are not called twice in the case scenario I brought forth here, please?

@backbone87
Copy link
Contributor

I agree with @d-ph, but i would keep buildTransitionBlockerList because some users (like me :)) want to show the user what needs to be done in advance and not after he tried to transit into a new place.

@lyrixx
Copy link
Member Author

lyrixx commented Feb 19, 2018

I can put back the exception in the apply method.

@lyrixx
Copy link
Member Author

lyrixx commented Feb 22, 2018

Actually, I the more I think to this issue, the more I think is not the right way to go.
Exception should remain exceptionnel and should not be used to control the flow.

If you have some perf issue with guard, The best way is to add a cache layer in your application. I should be really easy to implement, since we have a very nice cache component.

@d-ph
Copy link

d-ph commented Feb 24, 2018

I'm sorry, but I fail to see what the problem here is.

Firstly, the Workflow::apply() method, as it stands right now, is already throwing an exception (LogicException), when a transition isn't applied in the ::apply() method for whatever reason. My solution just makes sure that the reason is clearly stated -- whether the transition wasn't applied because it's missing or whether it wasn't applied because some Guards said "No". The same execution model and "principle", just more informative.

Secondly, throwing specific exceptions, as a way to knowing what the TransitionBlockers are, is an alternative to the solution of doing it with the if can(), then apply(). If devs want less performant solution but more ideology-compliant, then they can go for the if can(), then apply(). Other devs can catch exceptions from the apply() method. Everyone's happy.

Thirdly, as the old adage has it: "The two most difficult things in software development are: naming things and invalidating the caches". Sorry to sound harsh, but what you essentially said was: "I have personal/ideological issues with using the language's standard means to communicate errors in routines, but that's alright because you can fix the performance problems by using and maintaining caches, which is one of the two of the most annoying problems in software industry". Communicating errors in routines is not a new problem and we've all seen all the possible solutions out there already:

  1. Returning a valid value or an error code from a routine.
  2. Returning a valid value or false and offering a stateful getter for the last error in that routine (e.g. json_last_error())
  3. Throwing an exception.

None of these are pretty, but all of them solve the problem. I'd say it's highly contextual, as to which of these methods should be used when. And I'd say option 3. has the best "getting things done effectively"/ugliness factor in the case of Workflow::apply().

I have a feeling that the actual problem is in the fact, that in order to get ::apply() throw specific exceptions, my ugly sounding getEnabledTransitionsByNameOrTransitionBlockerList() would need to be brought over from my PR and for some reason this is being tried to be avoided at all cost. I admit the name of this method is ugly, and that it returns two different things depending on circumstances. However, it's just a private method, so there should be no issues with the fact that it's a highly specialised and optimised method to achieve a specific goal (i.e. not to run the Guards twice).

I understand that this is your code and your component, so eventually whichever approach is chosen is down to you. I just wanted to express my humble dev feedback. Thanks for taking it into your consideration and consciously deciding what's best for this component.

@andrewtch
Copy link
Contributor

As for #26581, the exception class should contain transition / workflow name instead merging them into string (that needs to be parsed to determine particular exception source when caught in somehow top-level exception handler).

@lyrixx
Copy link
Member Author

lyrixx commented Mar 19, 2018

@andrewtch Sure. It will be fixed with #26092

@lyrixx lyrixx force-pushed the workflow-blocker branch 3 times, most recently from 9219144 to 59c2f8b Compare March 20, 2018 14:52

public function __construct(string $transitionName, string $workflowName, TransitionBlockerList $transitionBlockerList)
{
parent::__construct(sprintf('The transition "%s" is not enabled for the workflow "%s".', $transitionName, $workflowName));
Copy link
Member

Choose a reason for hiding this comment

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

Transition "%s" is not enabled for workflow "%s".?

{
public function __construct(string $transitionName, string $workflowName)
{
parent::__construct(sprintf('The transition "%s" is not defined for the workflow "%s".', $transitionName, $workflowName));
Copy link
Member

Choose a reason for hiding this comment

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

Transition ... for workflow ...

private $code;

/**
* @var array This is useful if you would like to pass around the condition values, that
Copy link
Member

Choose a reason for hiding this comment

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

I would move this doc to the constructor phpdoc instead as this var is private.

}

/**
* Create a blocker, that says the transition cannot be made because it is
Copy link
Member

Choose a reason for hiding this comment

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

Creates a blocker that says ...

}

/**
* Create a blocker, that says the transition cannot be made because it has
Copy link
Member

Choose a reason for hiding this comment

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

same here (missing s, no comma)


/**
* Create a blocker, that says the transition cannot be made because it has
* blocked by the expression guard listener.
Copy link
Member

Choose a reason for hiding this comment

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

it has been blocked

}

/**
* Create a blocker, that says the transition cannot be made because of
Copy link
Member

Choose a reason for hiding this comment

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

same here as well

*/
private $parameters;

public function __construct(string $message, string $code, array $parameters = array())
Copy link
Member

Choose a reason for hiding this comment

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

We should probably document what code is and the different possibilities.

*/
public function can($subject, $transitionName);

/**
* Build a TransitionBlockerList to know why a transition is blocked.
Copy link
Member

Choose a reason for hiding this comment

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

Builds

* @param object $subject A subject
* @param string $transitionName A transition
*/
public function buildTransitionBlockerList($subject, $transitionName): TransitionBlockerList;
Copy link
Member

Choose a reason for hiding this comment

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

As this is a new method, you can use type hints here at least for $transitionName

@lyrixx
Copy link
Member Author

lyrixx commented Mar 20, 2018

@fabpot Thanks for the review. I have addressed your comments.

Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

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

with a minor comment

* Builds a TransitionBlockerList to know why a transition is blocked.
*
* @param object $subject A subject
* @param string $transitionName A transition
Copy link
Member

Choose a reason for hiding this comment

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

Could be removed

Copy link
Member Author

Choose a reason for hiding this comment

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

done 👍

@lyrixx lyrixx force-pushed the workflow-blocker branch from b8cfbfc to 2b8faff Compare March 20, 2018 18:12
@fabpot
Copy link
Member

fabpot commented Mar 20, 2018

Thank you @lyrixx.

@fabpot fabpot merged commit 2b8faff into symfony:master Mar 20, 2018
fabpot added a commit that referenced this pull request Mar 20, 2018
This PR was merged into the 4.1-dev branch.

Discussion
----------

[Workflow] Add transition blockers

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #24745 #24501
| License       | MIT

Commits
-------

2b8faff [Workflow] Cleaned the transition blocker implementations
4d10e10 [Workflow] Add transition blockers
@lyrixx lyrixx deleted the workflow-blocker branch March 20, 2018 21:12
@lyrixx
Copy link
Member Author

lyrixx commented Mar 21, 2018

And thanks @d-ph for the initial work ;)

fabpot added a commit that referenced this pull request Mar 21, 2018
This PR was merged into the 4.1-dev branch.

Discussion
----------

[Workflow] removed bc break

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? |
| Tests pass?   | yes
| Fixed tickets | #26076
| License       | MIT
| Doc PR        |

Commits
-------

685695d [Workflow] removed bc break
@dkarlovi
Copy link
Contributor

This looks like #23863? \o/

@fabpot fabpot mentioned this pull request May 7, 2018
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.

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