Давайте представим некоторый проект на GitHub, куда мы хотим оформить Pull Request. Здесь нас будет интересовать только тот огромный жизненный цикл нашего пулл реквеста, который он фактически может пройти с момента рождения до самого момента его принятия и мержа в основной код проекта.

image

Итак, если порассуждаем, то пулл реквест может иметь следующие варицации над состояниями, которые я специально усложнил, если не знать о 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
Поделиться с друзьями
-->

Комментарии (23)


  1. greabock
    06.04.2017 20:45

    Странно, в что материале ни разу не упоминается конечный автомат.


  1. pbatanov
    06.04.2017 22:23
    +2

    Ну, возможно, потому, что finite state machine является частным случаем сетей Петри (по крайней мере если судить по енвики)


  1. dgstudio
    07.04.2017 10:49

    Не менее интересно делать системы, в которых путь workflow можно формировать динамически, например из веб-интерфейса. В этом случае кастомер сам конфигурирует состояния, возможные переходы и правила "допустимости" перехода из одного состояния в то или иное новое. Вот один из примеров моих систем: https://github.com/dobryakov/workflow-system


    А вот и другая статья на тему, как можно отдать поток исполнения в руки кастомеру: https://www.dobryakov.com/blog/1838/


    1. VolCh
      07.04.2017 11:24

      Данный компонент позволяет и динамически формировать и юзером, и админом, и в базе хранить и с внешних сервисов по API получать, и в env-переменных задавать, конфиг лишь способ удобного задания статических параметров. Впрочем это к экосистеме Симфони в целом применимо — конфиги лишь способ задания статических параметров и из любого поддерживаемого формата в итоге всё равно компилируются (именно компилируются, а не заполняются какие-то структуры данных) в php-код, который можно и ручками писать сразу и любую динамику в нём делать.


  1. VolCh
    07.04.2017 11:51
    +1

    Давно посматриваю на компонент, но никак не могу встреть примеров реального использования в сложных процессах, не говоря о лучших практиках. Несколько вопросов на которые примеры плохо отвечают:


    • как лучше реализовать транзакции не просто состояние (place) процесса изменяющие, но и несущие полезную нагрузку, например отредактированную статью в транзакции "утверждение корректором"
    • как лучше реализовать воркфлоу, в которое вовлечено множество сущностей (возможно даже на старте флоу — неизвестное множество, которое к концу заполняется)
    • как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

    Такое ощущение, что его привязка к обычным сущностям — это лишь возможность для простейших случаев, которой лучше не пользоваться, а надо создавать отдельную сущность для хранения состояния именно процесса, которая уже как-то связана с состояниями участвующих в процессе сущностей. То есть на примере с пулл-реквестом нужно создавать отдельную сущность PullRequestWorkflow, которая с PullRequest связана один-к-одному, причём может даже односторонне, так что сам PullRequest о флоу и не знает. Но не понятно, изменение PullRequest должно быть реакцией на изменение (или попытку изменения) PullRequestWorkflow или наоборот.


    Использовал кто на практике в сложных флоу?


    1. Evsign
      07.04.2017 21:39

      Не уверен, что правильно понял все вопросы, но попробую предложить свои варианты решения.


      как лучше реализовать транзакции не просто состояние (place) процесса изменяющие, но и несущие полезную нагрузку, например отредактированную статью в транзакции "утверждение корректором"

      Использовать Command Bus. Создаётся 2 класса (команда и обработчик команды). Собственно команда и несёт в себе полезную нагрузку (пейлоад), это просто тупой класс с гетерами, а соответствующий обработчик производит манипуляции с этим пейлоадом в конце которой, происходит ->apply($article, 'next_state');
      https://habrahabr.ru/post/280512


      как лучше реализовать воркфлоу, в которое вовлечено множество сущностей (возможно даже на старте флоу — неизвестное множество, которое к концу заполняется)

      К сожалению не понял, что имеется ввиду)


      как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

      Евенты, команды, умная мидлваря для команд.


      1. Кидать евент try_to_publish в конце каждой комманды-транзакции (утверждение журналистом, утверждение корректором), в обработчике которого будет происходить проверка $workflow->can($article, 'publish'); и если тру, то кидаться команда PublishAtricle. Или продублировать проверку $workflow->can($article, 'publish'); в командах и если тру сразу кидать команду на паблишинг.


      2. Диспатчить команду PublishArticle, в конце каждой комманды-транзакции (утверждение журналистом, утверждение корректором), а проверку $workflow->can($object, 'state') вынести в миддлварю.


      3. Можно добавить в конфиг параметр auto: true/false для определённых переходов и написать умную мидлварю, которая будет проходиться по всем возможным переходам (те которые ->can($obj, 'state); //true) для сущности, и если переход помечен как auto: true, то диспатчить соответствующую команду.

      Я лично за последний вариант)


      1. Fesor
        09.04.2017 13:08

        Использовать Command Bus

        Может вы про CQRS? Там не команды записываются а ивенты которые произошли в контексте агрегатов сущностей. Вот тогда да, тогда можно будет делать $article->apply($nextState);. А воспринимать команды как ивенты — будет работать опять же только на очень простых задачах. Да и будут проблемы потом с расширением логики.


        1. Evsign
          09.04.2017 13:50

          Да, про CQRS, вернее про его часть.


          К сожалению не понимаю что вы хотели сказать и что побудило)
          Я не отождествлял команды с евентами, а писал про использование их в вместе в ответе на 3й вопрос. Да и то, только в 1м варианте, но в целом это никак не противоречит cqrs. К слову, cqrs очень хорошо сочетается с event-based programming.


          Там не команды записываются а ивенты которые произошли в контексте агрегатов сущностей. Вот тогда да, тогда можно будет делать $article->apply($nextState);.

          Можете расшифровать? Куда записываются, какие ивенты? Вы про event sourcing? Если да, то причём он тут?)


          Происходит какая-то бизнес-логика и диспатчится комманда (со статьёй в кач-ве пейлоада) --> миддлваря проверяет может ли выполниться комманда $w->can($command->article, 'approved_by_spellchecker'); --> обработчик чё-то делает, а потом в конце $w->apply($command->article, 'approved_by_spellchecker');. Всё. Что тут не так?)


          1. Fesor
            09.04.2017 16:20

            вернее про его часть.

            Command Bus — не обязательная часть CQRS. Это скорее простой способ заставить разработчика делать CQS. Аля void GRASP контроллеры, которые декларируют флоу конкретного юзкейса. У Дядей Бобов например во всяких чистых архитектурах подобные штуки назывались Interactors. Можно их же воспринимать как сервисы уровня приложения.


            К слову, cqrs очень хорошо сочетается с event-based programming.

            ну потому что это оно и есть собственно. Если вы имеете в виду только разделение операций на "операции чтения" и "операции записи" то это CQS.


            Куда записываются, какие ивенты? Вы про event sourcing? Если да, то причём он тут?)

            Event sourcing это самый простой способ решения проблемы eventual consistency. Помимо него можно делать похожие но чуть другие штуки.


            Записываются такие ивенты в какой-то сторадж. Обычно пишут в монгу или касандру но это не принципиально.


            Всё. Что тут не так?)

            1. откуда статья возьмется в пэйлоаде? Кто и на каком уровне ее вытащит и добавит в пэйлоад? То есть мы должны достать сущность еще на уровень выше?
            2. Как обработчик команды сделает работу? Из вашего описания у нас есть некая эдакая процедура "approved_by_spellchecker" которая на вход получает данные (инстанс Article) и что-то с ней делает. То есть либо наш хэндлер команды просто сконвертит действие в сообщение самому Article либо для того что бы делать дела мы должны полностью сломать инкапсуляцию Article.

            А ведь вопрос был в


            как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

            если бы мы говорили в терминах CQRS то команды генерили бы нам на выходе сэт ивентов ApprovedByJournalist и ApprovedByProofreader. Еще одна штука бы подписывалась на эти события и при достижении этого стэйта либо инициировала бы следующий транзишен стэйта либо просто строила бы проекцию сущности и писала бы например в табличку опубликованных статей.


            1. Evsign
              09.04.2017 17:21

              Блин, вы что-то всё усложнили сильно и зачем — непонятно)


              Вопроса было 3, я дал 3 ответа, причём последний содержит 3 варианта. Возможно я плохо их отформатировал и вы подумали, что пронумерованный список это соответственные ответы на все 3 вопроса. Пронумерованный список относится только к последнему вопросу. И естественно, я не углублялся в детали и не расписывал весь флоу обработки запроса в вакууме с прикручиванием cqrs или cqs, ведь это не относится к вопросам.


              откуда статья возьмется в пэйлоаде? Кто и на каком уровне ее вытащит и добавит в пэйлоад? То есть мы должны достать сущность еще на уровень выше?

              какая мне разница, вопрос о другом!) Где-то в коде, хоть в контроллере, создастся какая-то команда, в которую положат статью, потому, что это будет обязательный параметр конструктора, т.к. статья требуется для обработки данной команды, и задиспатчат.


              Как обработчик команды сделает работу? Из вашего описания у нас есть некая эдакая процедура "approved_by_spellchecker" которая на вход получает данные (инстанс Article) и что-то с ней делает.

              Опять, какая мне разница?) Да, есть комманда(простой класс с необходимым пейлоадом для обработки), которая отправляется обработчику, который выполняет какую-то бизнес-логику и делает транзишн в следующее состояние в конце работы, если всё ок.


              А ведь вопрос был в

              Это был 3й вопрос. И для его решения я бы сделал post middleware, которая автоматически будет проверять сущности из пейлоада команд и диспатчить новые команды для авматических переходов. Или да, можно генерить сет евентов, о чём я говорил в 1м варианте ответа на 3й вопрос.


              1. Fesor
                09.04.2017 17:32

                создастся какая-то команда, в которую положат статью

                если у вас УЖЕ есть статья то вам не нужно городить команды. Мы УЖЕ потеряли весь профит от Command Bus и теперь нам намного проще будет вызывать методы сущности нежели городить еще один слой кода.


                Опять, какая мне разница?)

                Профит то в чем? Ну то есть, зачем городить целую шину команд, которую можно заменить одним вызовом метода сущности которая уже сама способна разобраться можно ей менять стэйт или нет.


                А так мы просто размазали эту весьма тривиальную логику еще больше по системе. Что не есть гуд.


                1. Evsign
                  09.04.2017 18:31

                  если у вас УЖЕ есть статья то вам не нужно городить команды. Мы УЖЕ потеряли весь профит от Command Bus и теперь нам намного проще будет вызывать методы сущности нежели городить еще один слой кода.

                  Почему? Где вы предлагаете дёргать методы сущности в соответствии с какой-то бизнес логикой?
                  В контролере? Контроллер раздуется, получится лапшекод и жесть, которую потом хз как переиспользовать. А команды можно кинуть будет и через джобы потом, если потребуется. И тестировать есть что. Да это я думаю вы и так понимаете.
                  В сервисах? Ну это кому как, на вкус как говорится) Ну кода ещё по-меньше будет.


                  Профит то в чем? Ну то есть, зачем городить целую шину команд, которую можно заменить одним вызовом метода сущности которая уже сама способна разобраться можно ей менять стэйт или нет.

                  Ну я подразумеваю, что перед переходом возможна какая-то нетривиальная логика после отработки которой и должен осуществиться переход. Если это запихать в сущность, то это получится не сущность, а сервис какой-то или вообще хз что. Да даже простая операция записи в базу. Сущность сама себя в базу записывать будет?) Даже с активрекордом такое, я думаю, лучше не делать, т.к. выглядит как побочный эффект.


                  1. Fesor
                    09.04.2017 20:42

                    В контролере? Контроллер раздуется, получится лапшекод и жесть, которую потом хз как переиспользовать.

                    но вы же уже достаете в контроллере репозиторий, достаете сущность по запросу и только потом формируете команду. Так что зоны ответственности уже размазаны.


                    Если что у меня обычно есть хэндлеры команд и команды, в них была бы только айдишка сущности а не вся сущность.


                    Если это запихать в сущность, то это получится не сущность, а сервис какой-то или вообще хз что.

                    доменные ивенты вам помогут. Не предлагаете же вы делать сущности тупыми property-bag-ами?


                    Сущность сама себя в базу записывать будет?)

                    репозитории, unit of work. Сущность ничего не знает о базе, она знает только о том что у нее есть стэйт и только ей дано знать как этот стэйт должен поменяться.


                    1. Evsign
                      09.04.2017 22:52

                      доменные ивенты вам помогут. Не предлагаете же вы делать сущности тупыми property-bag-ами?

                      Ну в моём представлении это проперти бэг + методы которые манипулируют этими проперти. Ну т.е. стейт и то как этот стейт менять, как и вы и упомянули в конце.


                      но вы же уже достаете в контроллере репозиторий, достаете сущность по запросу и только потом формируете команду. Так что зоны ответственности уже размазаны.
                      Если что у меня обычно есть хэндлеры команд и команды, в них была бы только айдишка сущности а не вся сущность.

                      Согласен, ток айдишку ложить в команду — лучше. Но как это всё относится к самым первоначальным вопросам?) Вытянуть статью в команде или положить статью в команду где-то выше — это несущественный момент реализации в контексте основных вопросов.


                      Сущность ничего не знает о базе, она знает только о том что у нее есть стэйт и только ей дано знать как этот стэйт должен поменяться.
                      Ваще с этим не спорю и не ставил под сомнения. К чему вы это всё пишете?)

                      Короче, какой-то холивар на пустом месте) VolCh спрашивал:


                      1. Как организовать транзакции с пейлоадом? Я предложил команды (статья или айдишка статьи в пейлоаде — в данный момент не суть). И из всего что вы написали, я так и не понял, что здесь не так.


                      2. Я не понял вопроса.


                      3. Как сделать автонакатывание перехода? Я предложил евенты, миддлвари для команд.

                      Если хотите дальше продолжать, то давайте по пунктам и по делу, где и что не так. Или ещё лучше, предложите ваш вариант решения этих вопросов, что, думаю, будет всем интересно.


                      1. Fesor
                        09.04.2017 23:47

                        Ну т.е. стейт и то как этот стейт менять, как и вы и упомянули в конце.

                        я несколько другое упомянул. Я не про сеттеры, как возможность извне поменять стэйт объекта. Я про то что код использующий объект вообще ничего не должен знать о стэйте. Инкапсуляция и все такое.


                        Но как это всё относится к самым первоначальным вопросам?)

                        Моя претензия была именно к command bus так как проблему это не решает и из вашего описания создает новые.


                        Я предложил команды

                        это вопрос в контексте компонента workflow. Ваше предложение не отвечает на вопрос.


                        Я предложил евенты, миддлвари для команд.

                        ну или адаптеры и просто слать сообщения объектам. Речь опять же только про workflow компонент. потому ваше предложение юзать шину команд ни разу не отвечает на поставленные вопросы.


                        1. Evsign
                          10.04.2017 00:27

                          Я не про сеттеры

                          Я про сеттеры что ли? Я про нормальные доменные методы...


                          Моя претензия была именно к command bus так как проблему это не решает и из вашего описания создает новые.

                          А я пытаюсь всё это время узнать почему? и какие новые проблемы?


                          это вопрос в контексте компонента workflow. Ваше предложение не отвечает на вопрос.

                          На мой взгляд это вопрос в контексте приложения. Смысл рассматривать компонент в отрыве от приложения и того как он там используется?


                          Вопросы были:


                          1. Как организовать транзакции в которые можно запихнуть какой-то пейлоад? Я так понял транзакции имелось ввиду не в терминах автора статьи… Что корректней называть переходом.


                          2. Как сделать автопереход на следующий плейс, при достижении всех необходимых условий для перехода?

                          С command bus + workflow и транзакции с пейлоадом тебе, и автопереходы. И я понимаю, что это не единственный возможный вариант...


                  1. Fesor
                    09.04.2017 20:50

                    т.к. выглядит как побочный эффект.

                    так в этом же суть "команд". Они создают побочные эффекты. Любая операция связанная с взаимодействием с "внешним миром" по отношению к текущему процессу, например запись, и есть побочный эффект.


  1. Quber
    07.04.2017 18:29

    Тоже поглядываю на компонент, но не могу понять где его применять. У меня в голове несколько вопросов:


    1. Правильно ли я понимаю, что компонент workflow можно использовать для чего угодно, а не только для pull request? Просто везде на сайтах именно этот пример показывают.
    2. Не проще ли иметь просто поле с состоянием, чтобы его переключать? В чем удобство?


    1. VolCh
      07.04.2017 18:53

      1. Да, ещё примеры на статьях бывают :)
        2.1. Ограничивается граф возможных направлений перехода из одного состояния в другое, чего "тупой" setStatus сделать не может.
        2.2. Граф можно задавать в конфиге, а не в коде
        2.3. Можно иметь одновременно несколько состояний для параллельных подпроцессов
        2.4. "Бесплатная" интеграция через события с остальным приложением.

      как-то так


      1. pbatanov
        07.04.2017 19:56
        +1

        + Визуализация
        + Guads (3.3)

        2.2. Граф можно задавать в конфиге, а не в коде


        Вот чего я не могу понять — а можно ли хранить граф НЕ в коде. Если можно, то тогда можно будет делать крутые штуки для бизнес-процессов, а-ля workflows в JIRA


        1. Evsign
          08.04.2017 01:25

          Можно хранить всё как хочешь и где хочешь, и формировать динамически.


          1. pbatanov
            08.04.2017 16:13

            Нашел, действительно, спасибо


    1. php_freelancer
      07.04.2017 21:39

      Это не просто переключалка. Это гарантия того, что объект всегда будет валиден с точки зрения бизнес логики приложения.