Сети Петри с Symfony а-ля WorkFlow компонент
Итак, если порассуждаем, то пулл реквест может иметь следующие варицации над состояниями, которые я специально усложнил, если не знать о WorkFlow и смотреть на подобное тз:
1. Открыт
2. Находится в проверке в Travis CI, причем может попасть туда после того как были сделаны какие-то исправления или любые изменения, связанные с нашим Pull Request, ведь проверить-то надо все, не так ли?
3. Ждет Review только после того как была сделана проверка в Travis CI
3.1. Требует обновлений кода после того как была сделана проверка в Travis CI
4. Требует изменения после Review
5. Принят после Review
6. Смержен после Review
7. Отклонен после Review
8. Закрыт после того, как был отклонен после Review
9. Открыт заново после того как был закрыт, после того как был отклонен, после того как было проведено Review
10. Изменения после того как был помечен «Требует изменений», после того как было проведено Review, при этом после этого он снова должен попасть в Travis CI (пункт 2), а от Review снова может с ним случиться только те состояния, которые мы описали выше
Жесть, правда?
То, что в квадратах — мы будем называть транзакциями, тем временем всё то, что находится в кругах — это те самые состояния, о которых мы ведем речь. Транзакция — это возможность перехода из определенного состояния (или нескольких состояний сразу) в другое состояние.
Здесь и вступает в игру WorkFlow компонент, который будет помогать нам управлять состояниями объектов внутри нашей системы. Смысл в том, что сами состояния задает разработчик, тем самым гарантируя, что данный объект всегда будет валиден с точки зрения бизнес логики нашего приложения.
Если человеческим языком, то пулл реквест никогда не сможет быть смержен, если он не прошел заданный нами ОБЯЗАТЕЛЬНЫЙ путь до определенного момента (от проверки в тревис и ревью до его принятия и самого мержа).
Итак, давайте создадим сущность PullRequest и зададим для неё правила перехода из одних состояний в другие.
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;
}
}
Вот как это будет выглядеть, когда ты знаешь что такое WorkFlow:
# 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
Так же как и на картинке, мы задаем определенные состояния, в которых фактически может прибывать наша сущность (framework.workflow.pull_request.places): start, coding, travis, review, merged, closed и транзакции (framework.workflow.pull_request.transactions) с описанием, при каком условии объект может попасть в это состояние: submit, update, wait_for_review, request_change, accept, reject, reopen.
А теперь снова вернемся в жизнь:
Submit — это транзакция перехода из начального состояния в состояние проверки изменений в Travis CI.
Это наше самое первое действие, здесь мы оформляем наш пулл реквест и после этого Travis CI начинает проверять наш код на валидность.
Update — транзакция перехода из состояний coding (состояние написания кода), travis (состояние проверки на Travis CI), review (Состояние, когда происходит review кода) в состояние проверки Travis.
Это то действие, которое говорит системе, что нужно снова все перепроверить после каких-либо изменений в нашем pull request, т. е. в том, что готовится смержится в мастер.
Wait For Review — транзакция перехода из состояние Travis в состояние Review.
То бишь действие, когда мы запушили свой пулл реквест и он уже проверен Travis-ом, теперь пора программистам проекта взглянуть на наш код — сделать его ревью и принять решение что с этим делать дальше.
Request_Change — транзакция перехода состояния из Review в Coding.
Т.е. тот момент, когда (к примеру) команде проекта не понравилось то, как мы решили поставленную задачу и они хотят увидеть другое решение и мы вносим какие-то изменения в виде исправлений снова.
Accept — транзакция перехода состояния из Review в Merged, конечная точка, которая не имеет после себя никаких возможных транзакций.
Момент, когда программистам проекта нравится наше решение и они его мержат в проект.
Reject — транзакция перехода состояния из Review в Closed.
Момент, когда программисты не посчитали нужным принимать наш pull request по каким-либо причинам.
Reopen — транзакция перехода состояния Сlosed в состояние Review.
Например когда команда программистов проекта пересмотрела наш пулл реквест и решила его пересмотреть.
Теперь давайте уже наконец-таки напишем хоть какой-нибудь код:
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
При этом, если абстрагироваться, то иногда бывает так, что сам объект может иметь несколько состояний одновременно. Помимо state_machine мы можем прописать нашему объекту тип workflow, что позволит одновременно иметь несколько статусов у одного объекта. Примером из жизни может послужить ваша первая публикация на хабре, которая одновременно может иметь статусы, например: «Мне нужна проверка на плагиат», «Мне нужна проверка на качество» и которая может перейти в статус «Опубликована» только после того как все эти проверки пройдены, ну это конечно при условии, что все эти процессы не автоматизированы, но мы сейчас ведем речь не об этом.
Для примера создадим новую сущность Article в нашей системе.
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;
}
}
Теперь создадим для него WorkFlow конфигурацию:
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
Давайте посмотрим как будит выглядеть наш код:
$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, все проверки пройдены
Вы так же без проблем можете визуализировать то, что вы только что сделали, для этого мы будем пользоваться www.graphviz.org — ПО для визуализации графов, который на вход принимает в себя данные вида:
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"];
}
Конвертировать наш граф в такой формат можно как с помощью PHP:
$dumper = new \Symfony\Component\Workflow\Dumper\GraphvizDumper();
echo $dumper->dump($stateMachine->getDefinition());
Так и с помощью готовой команды
php bin/console workflow:dump pull_request > out.dot
dot -Tpng out.dot -o graph.png
graph.png будет иметь следующий вид для PullRequest:
и для Article:
Дополнение:
Уже с выходом 3.3 в stable мы сможем использовать 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
Комментарии (2)
6 апреля 2017 в 20:45 (комментарий был изменён)
0↑
↓
Странно, в что материале ни разу не упоминается конечный автомат.
6 апреля 2017 в 22:23
+1↑
↓
Ну, возможно, потому, что finite state machine является частным случаем сетей Петри (по крайней мере если судить по енвики)