Petri nets with Symfony a la WorkFlow component

  • Tutorial
Let's submit some project on GitHub where we want to issue a Pull Request. Here we will be interested only in the huge life cycle of our pull request that it can actually go through from the moment of birth to the moment of its adoption and merge into the main code of the project.

image

So, if we think about it, then the pull request can have the following variations over the states, which I especially complicate, if I don’t know about WorkFlow and look at something like this:

1. Open
2. It is being checked in Travis CI, and it can get there after some corrections were made or any changes related to our Pull Request, because you need to check everything, right?
3. Waits for Review only after verification has been done in Travis CI
3.1. Require code updates after the check has been made in the CI Travis
4. requires changes after the Review,
5. Adopted after Review,
6. Smerzhen after Review,
7. Rejected after the Review,
8. Closed after was rejected after the Review,
9. reopened after how it was closed, after it was rejected, after Review
10. was made . Changes after it was marked “Needs changes”, after Review was carried out, and after that it should again go to Travis CI (paragraph 2 ), and from the Review could happen to him again, only those conditions which we have described above

there, right?

That in squares - we will call transactions, in the meantime, everything that is in the circles is the very states that 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 within our system. The point is that the developer sets the states themselves, thereby guaranteeing that this object will always be valid from the point of view of the business logic of our application.

If the language is human, then the pull request can never be subjugated if it did not follow the MANDATORY path we set to a specific point (from checking in travis and review to its adoption and merge itself).

So, let's create a PullRequest entity and set rules for transition from one state to another for it.

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;
    }
}

Here's what 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 define certain states in which our entity (framework.workflow.pull_request.places) can actually arrive: start, coding, travis, review, merged, closed and transactions (framework.workflow.pull_request.transactions ) with a description of the condition under which an object can fall into this state: submit, update, wait_for_review, request_change, accept, reject, reopen.

And now let's get back to life:

Submit is a transaction of transition from the initial state to the state of verification of changes in Travis CI.

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

Update - transaction of transition from coding (code writing state), travis (state of verification for Travis CI), review (state when code review occurs) to Travis verification state.
This is the action that tells the system that it is necessary to double-check everything again after any changes in our pull request, that is, that it is preparing to contend in the master.

Wait For Review - transaction from the Travis state to the Review state.
That is, the action, when we launched our pull request and it has already been tested by Travis, now it’s time for the project programmers to look at our code - to review it and decide what to do next.

Request_Change is a state transition transaction from Review to Coding.
Those. the moment when (for example) the project team didn’t like the way we solved the task and they want to see another solution and we are making some changes in the form of corrections again.

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

Reject is a state transition transaction from Review to Closed.

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

Reopen - transaction of transition of the closed state to the Review state.

For example, when a team of project programmers reviewed our pull request and decided to review 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

Moreover, if we ignore it, sometimes it happens that the object itself can have several states at the same time. In addition to state_machine, we can specify a workflow type for our object, which will allow us to have several statuses for one object at a time. An example from life is your first publication on the hub, which can simultaneously have statuses, for example: “I need a plagiarism check”, “I need a quality check” and which can go to the “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 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 wakes up to look:

$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 just done without any problems, for this we will use www.graphviz.org - graph visualization software that receives 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 using PHP:

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

So with the help of a ready-made 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:


Supplement:

Already with 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

Also popular now: