📜 ⬆️ ⬇️

Petri net with symfony a la WorkFlow component

Let's present some project on GitHub where we want to issue Pull Request. Here we will be interested only in the huge life cycle of our pull request, which it can actually go from the moment of birth to the very moment of its acceptance and merge into the main project code.

image

So, if we argue, then the pull request can have the following variations on the states, which I have specially complicated, if you don’t know about WorkFlow and look at the similar terms of reference:
')
1. Opened
2. Is in check in Travis CI, and can get there after some corrections or any changes related to our Pull Request were made, because you need to check everything, isn't it?
3. Waiting for Review only after the check was done in Travis CI
3.1. Requires code updates after checking in Travis CI
4. Requires change after Review
5. Adopted after Review
6. Smezhen after Review
7. Declined after Review
8. Closed after rejected after Review.
9. Reopened after it was closed, after it was rejected, after the Review was conducted.
10. Changes after the “Require Changes” was labeled, after the Review was conducted, then after that it should again go to Travis CI (item 2), and from the Review again only those conditions that we described above

Tin, right?

The fact that in the squares - we will call transactions, in the meantime, all that is in the circles - these are the very states we are talking about. A transaction is the ability to transition from a certain state (or several states at once) to another state.
This is where the WorkFlow component comes into play, which will help us manage the states of objects inside our system. The meaning is that the states themselves are set by the developer, thereby ensuring that this object will always be valid from the point of view of the business logic of our application.

If it is in human language, then a pull request can never be confused if it has not passed the MANDATORY path that we have specified up to a certain point (from checking in Tragedy and Review to its adoption and the merge itself).

So, let's create the PullRequest entity and set its transition rules from one state to another.

namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="pull_request") * @ORM\Entity(repositoryClass="AppBundle\Repository\PullRequestRepository") */ class PullRequest { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $currentPlace; /** * @return int */ public function getId() { return $this->id; } /** * @return PullRequest */ public function setCurrentPlace($currentPlace) { $this->currentPlace = $currentPlace; return $this; } /** * @return string */ public function getCurrentPlace() { return $this->currentPlace; } } 

This is how it will look like when you know what WorkFlow is:

 # app/config/config.yml framework: workflows: pull_request: type: 'state_machine' marking_store: type: 'single_state' argument: 'currentPlace' supports: - AppBundle\Entity\PullRequest places: - start - coding - travis - review - merged - closed transitions: submit: from: start to: travis update: from: [coding, travis, review] to: travis wait_for_review: from: travis to: review request_change: from: review to: coding accept: from: review to: merged reject: from: review to: closed reopen: from: closed to: review 

As in the picture, we set certain states in which our entity can actually arrive (framework.workflow.pull_request.places): start, coding, travis, review, merged, closed and transactions (framework.workflow.pull_request.transactions ) with a description of the condition under which the object can fall into this state: submit, update, wait_for_review, request_change, accept, reject, reopen.

And now back in life:

Submit is a transition from the initial state to the change validation state in the Travis CI.

This is our very first action, here we make our pull request and after that Travis CI starts checking our code for validity.

Update - transition from coding state (code writing state), travis (Travis CI check status), review (Code review status) to Travis check status.
This is the action that tells the system that it is necessary to recheck everything again after any changes in our pull request, i.e., that it is preparing to submit to the master.

Wait For Review - transaction of transition from the state of Travis to the state of Review.
I mean the action, when we launched our pull request and it was already checked by Travis, now it's time for the project programmers to take a look at our code - make it a review and decide what to do with it next.

Request_Change is a state transition transaction from Review to Coding.
Those. that moment when (for example) the project team did not like the way we solved the task and they want to see a different solution and we make some changes in the form of corrections again.

Accept is a state transition from Review to Merged, an endpoint that does not have any possible transactions after it.
The moment when project programmers like our solution and they merge it into the project.

Reject - a state transition transaction from Review to Closed.

The moment when programmers did not consider it necessary to accept our pull request for any reason.

Reopen - the transition from the Closed state to the Review state.

For example, when the project team of programmers revised our pull request and decided to revise it.

Now let's finally write at least some code:

 use AppBundle\Entity\PullRequest; use Symfony\Component\Workflow\Exception\LogicException; $pullRequest = new PullRequest(); //    $stateMachine = $this->getContainer()->get('state_machine.pull_request'); $stateMachine->can($pullRequest, 'submit'); //true $stateMachine->can($pullRequest, 'accept'); //false try { //    start   travis $stateMachine->apply($pullRequest, 'submit'); } catch(LogicException $workflowException) {} $stateMachine->can($pullRequest, 'update'); //true $stateMachine->can($pullRequest, 'wait_for_review'); //true $stateMachine->can($pullRequest, 'accept'); //false try { //    update   review $stateMachine->apply($pullRequest, 'wait_for_review'); } catch(LogicException $workflowException) {} $stateMachine->can($pullRequest, 'request_change'); //true $stateMachine->can($pullRequest, 'accept'); //true $stateMachine->can($pullRequest, 'reject'); //true $stateMachine->can($pullRequest, 'reopen'); //false try { //    update   review $stateMachine->apply($pullRequest, 'reject'); } catch(LogicException $workflowException) {} $stateMachine->can($pullRequest, 'request_change'); //false $stateMachine->can($pullRequest, 'accept'); //false $stateMachine->can($pullRequest, 'reject'); //false $stateMachine->can($pullRequest, 'reopen'); //true -    pull request echo $pullRequest->getCurrentPlace(); //closed try { //   -        $stateMachine->apply($pullRequest, 'reject'); } catch(LogicException $workflowException) { echo '   !!! :('; } $stateMachine->apply($pullRequest, 'reopen'); echo $pullRequest->getCurrentPlace(); //review 

At the same time, if we abstract, it sometimes happens that the object itself can have several states at the same time. In addition to the state_machine, we can prescribe the type of workflow to our object, which allows us to simultaneously have several statuses for one object. An example of life can serve as your first publication on Habré, which can simultaneously have statuses, for example: "I need a plagiarism check", "I need a quality check" and which can become "Published" status only after all these checks passed, well, of course, provided that all these processes are not automated, but we are not talking about that now.

For example, create a new Article entity in our system.

 use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="article") * @ORM\Entity(repositoryClass="AppBundle\Repository\ArticleRepository") */ class Article { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="simple_array") */ private $currentPlaces; public function getId() { return $this->id; } public function setCurrentPlaces($currentPlaces) { $this->currentPlaces = $currentPlaces; return $this; } public function getCurrentPlaces() { return $this->currentPlaces; } } 

Now let's create a WorkFlow configuration for it:

 article: supports: - AppBundle\Entity\Article type: 'workflow' marking_store: type: 'multiple_state' argument: 'currentPlaces' places: - draft - wait_for_journalist - approved_by_journalist - wait_for_spellchecker - approved_by_spellchecker - published transitions: request_review: from: draft to: - wait_for_journalist - wait_for_spellchecker journalist_approval: from: wait_for_journalist to: approved_by_journalist spellchecker_approval: from: wait_for_spellchecker to: approved_by_spellchecker publish: from: - approved_by_journalist - approved_by_spellchecker to: published 

Let's see how our code looks like:

 $article = new Article(); $workflow = $this->getContainer()->get('workflow.article'); $workflow->apply($article, 'request_review'); /* array(2) { ["wait_for_journalist"]=> int(1) ["wait_for_spellchecker"]=> int(1) } */ var_dump($article->getCurrentPlaces()); //,   ! $workflow->apply($article, 'journalist_approval'); /* array(2) { ["wait_for_spellchecker"]=> int(1) ["approved_by_journalist"]=> int(1) } */ var_dump($article->getCurrentPlaces()); var_dump($workflow->can($article, 'publish')); //false,         $workflow->apply($article, 'spellchecker_approval'); var_dump($workflow->can($article, 'publish')); //true,    

You can also visualize what you have just done without any problems, for this we will use www.graphviz.org - graph visualization software, which, at the entrance, takes data of the form:

 digraph workflow { ratio="compress" rankdir="LR" node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"]; edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"]; place_start [label="start", shape=circle, style="filled"]; place_coding [label="coding", shape=circle]; place_travis [label="travis", shape=circle]; place_review [label="review", shape=circle]; place_merged [label="merged", shape=circle]; place_closed [label="closed", shape=circle]; transition_submit [label="submit", shape=box, shape="box", regular="1"]; transition_update [label="update", shape=box, shape="box", regular="1"]; transition_update [label="update", shape=box, shape="box", regular="1"]; transition_update [label="update", shape=box, shape="box", regular="1"]; transition_wait_for_review [label="wait_for_review", shape=box, shape="box", regular="1"]; transition_request_change [label="request_change", shape=box, shape="box", regular="1"]; transition_accept [label="accept", shape=box, shape="box", regular="1"]; transition_reject [label="reject", shape=box, shape="box", regular="1"]; transition_reopen [label="reopen", shape=box, shape="box", regular="1"]; place_start -> transition_submit [style="solid"]; transition_submit -> place_travis [style="solid"]; place_coding -> transition_update [style="solid"]; transition_update -> place_travis [style="solid"]; place_travis -> transition_update [style="solid"]; transition_update -> place_travis [style="solid"]; place_review -> transition_update [style="solid"]; transition_update -> place_travis [style="solid"]; place_travis -> transition_wait_for_review [style="solid"]; transition_wait_for_review -> place_review [style="solid"]; place_review -> transition_request_change [style="solid"]; transition_request_change -> place_coding [style="solid"]; place_review -> transition_accept [style="solid"]; transition_accept -> place_merged [style="solid"]; place_review -> transition_reject [style="solid"]; transition_reject -> place_closed [style="solid"]; place_closed -> transition_reopen [style="solid"]; transition_reopen -> place_review [style="solid"]; } 

You can convert our graph to this format as with PHP:

 $dumper = new \Symfony\Component\Workflow\Dumper\GraphvizDumper(); echo $dumper->dump($stateMachine->getDefinition()); 

So with the help of the finished team

  php bin/console workflow:dump pull_request > out.dot dot -Tpng out.dot -o graph.png 

graph.png will look like this for PullRequest:


and for Article:


Addition:

Already with the release of 3.3 in stable we can use guard:

 framework: workflows: article: audit_trail: true supports: - AppBundle\Entity\Article places: - draft - wait_for_journalist - approved_by_journalist - wait_for_spellchecker - approved_by_spellchecker - published transitions: request_review: guard: "is_fully_authenticated()" from: draft to: - wait_for_journalist - wait_for_spellchecker journalist_approval: guard: "is_granted('ROLE_JOURNALIST')" from: wait_for_journalist to: approved_by_journalist spellchecker_approval: guard: "is_fully_authenticated() and has_role('ROLE_SPELLCHECKER')" from: wait_for_spellchecker to: approved_by_spellchecker publish: guard: "is_fully_authenticated()" from: - approved_by_journalist - approved_by_spellchecker to: published 

Source: https://habr.com/ru/post/325758/


All Articles