Как использовать рабочий поток

Как использовать рабочий поток

Рабочий поток - это процесс жизненного цикла, через который проходят ваши объекты. Каждый шаг или этап процесса называется местом. Вы также определяете переходы, которые описывают действие перехода из одного места в другое.

Набор мест и переходов создаёт определение. Рабочему потоку необходимо Definition (определение) и способ написать состояния объектов (например, сущность класса MarkingStoreInterface.)

Рассмотрите следующий пример для записи блога. Запись может иметь такие места: "черновик", "обзор", "отвергнутый", "опубликованный". Вы можете определить рабочий поток следующим образом:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
framework:
    workflows:
        blog_publishing:
            type: 'workflow' # or 'state_machine'
            marking_store:
                type: 'multiple_state' # or 'single_state'
                arguments:
                    - 'currentPlace'
            supports:
                - AppBundle\Entity\BlogPost
            places:
                - draft
                - review
                - rejected
                - published
            transitions:
                to_review:
                    from: draft
                    to:   review
                publish:
                    from: review
                    to:   published
                reject:
                    from: review
                    to:   rejected
1
2
3
4
5
6
7
class BlogPost
{
    // Это свойство используется хранилищем маркировки
    public $currentPlace;
    public $title;
    public $content;
}

Note

Тип хранилища маркировки может быть множественным и единственным состоянием ("multiple_state" или "single_state"). Единственное состояние не поддерживает модель, которая нахолится в нескольких местах одновременно.

Tip

Атрибуты type (значение по умолчанию single_state) и arguments (значение по умолчанию marking) опции marking_store необязательны. Если их опустить, будут использованы их значения по умолчанию.

С этим рабочим потоком, названным blog_publishing, вы можете получить помощь в решении того, какие действия разрешены в записи блога:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$post = new \AppBundle\Entity\BlogPost();

$workflow = $this->container->get('workflow.blog_publishing');
$workflow->can($post, 'publish'); // False
$workflow->can($post, 'to_review'); // True

// Update the currentState on the post
try {
    $workflow->apply($post, 'to_review');
} catch (LogicException $e) {
    // ...
}

// Смотрите все доступные переходы для записи в текущем состоянии
$transitions = $workflow->getEnabledTransitions($post);

Использование событий

Чтобы сделать ваши рабочие потоки еще более мощными, вы можете построить объект Workflow с EventDispatcher. Теперь вы можете создавать слушателей событий для блокировки переходов (т.е. в зависимости от данных в записи блога). Развёртываются следующие события:

  • workflow.leave
  • workflow.[workflow name].leave
  • workflow.[workflow name].leave.[place name]
  • workflow.transition
  • workflow.[workflow name].transition
  • workflow.[workflow name].transition.[transition name]
  • workflow.enter
  • workflow.[workflow name].enter
  • workflow.[workflow name].enter.[place name]
  • workflow.entered
  • workflow.[workflow name].entered
  • workflow.[workflow name].entered.[place name]
  • workflow.announce
  • workflow.[workflow name].announce
  • workflow.[workflow name].announce.[transition name]

Вот пример того, как включать логирование каждый раз, когда рабочий поток "blog_publishing" покидает место:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;

class WorkflowLogger implements EventSubscriberInterface
{
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onLeave(Event $event)
    {
        $this->logger->alert(sprintf(
            'Blog post (id: "%s") performed transaction "%s" from "%s" to "%s"',
            $event->getSubject()->getId(),
            $event->getTransition()->getName(),
            implode(', ', array_keys($event->getMarking()->getPlaces())),
            implode(', ', $event->getTransition()->getTos())
        ));
    }

    public static function getSubscribedEvents()
    {
        return array(
            'workflow.blog_publishing.leave' => 'onLeave',
        );
    }
}

Защитные события

Существует особенный тип событий, под названием "защитные собтия". Их слушатели событий вызываются каждый раз, когда выполняется вызов Workflow::can, Workflow::apply или Workflow::getEnabledTransitions. С защитными собтиями вы можете добавлять пользовательскую логику для того, чтобы решить, какие переходы валидны, а какие - нет. Вот список имён защитных событий.

  • workflow.guard
  • workflow.[workflow name].guard
  • workflow.[workflow name].guard.[transition name]

Смотрите пример, чтобы убедиться, что ни одна запись блога без заголовка не будет перенесена в состояние "обзора":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class BlogPostReviewListener implements EventSubscriberInterface
{
    public function guardReview(GuardEvent $event)
    {
        /** @var \AppBundle\Entity\BlogPost $post */
        $post = $event->getSubject();
        $title = $post->title;

        if (empty($title)) {
            // Записи без заголовок не должны быть допущены
            $event->setBlocked(true);
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            'workflow.blogpost.guard.to_review' => array('guardReview'),
        );
    }
}

Методы событий

Каждое событие рабочего потока - это экземпляр Event. Это означает, что каждое событие имеет доступ к следующей информации:

getMarking()
Возвращает Marking рабочего потока.
getSubject()
Возвращает объект, который развёртывает событие.
getTransition()
Возвращает Transition, который развёртывает событие.
getWorkflowName()

Возвращает строку с именем рабочего потока, который вызвал событие.

3.3

Метод getWorkflowName() был представлен в Symfony 3.3.

For Guard Events, there is an extended class GuardEvent. This class has two more methods:

isBlocked()
Возвращается, если переход заблокирован.
setBlocked()
Устанавливает заблокированное значение.

Использование в Twig

Symfony определяет несколько функций Twig для управления рабочими потоками и уменьшения необходимости логики домена в ваших шаблонах:

workflow_can()
Возвращает true, если данный объект может выполнить данный переход.
workflow_transitions()
Возвращает массив со всеми переходами, включенными в данном объекте.
workflow_marked_places()
Возвращает массив с именами мест данной маркировки.
workflow_has_marked_place()
Возвращает true, если маркировка данного объекта имеет данное состояние.
.. versionadded:: 3.3
Функции workflow_marked_places() и workflow_has_marked_place() были представлены в Symfony 3.3.

Следующий пример иллюстрирует эти функции в действии:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<h3>Actions</h3>
{% if workflow_can(post, 'publish') %}
    <a href="...">Publish article</a>
{% endif %}
{% if workflow_can(post, 'to_review') %}
    <a href="...">Submit to review</a>
{% endif %}
{% if workflow_can(post, 'reject') %}
    <a href="...">Reject article</a>
{% endif %}

{# Или закольцевать через включенные переходы #}
{% for transition in workflow_transitions(post) %}
    <a href="...">{{ transition.name }}</a>
{% else %}
    No actions available.
{% endfor %}

{# Проверить, находится ли объект в некотором особенном месте #}
{% if workflow_has_marked_place(post, 'to_review') %}
    <p>This post is ready for review.</p>
{% endif %}

{# Проверить, если некоторое место было маркировано в объекте #}
{% if 'waiting_some_approval' in workflow_marked_places(post) %}
    <span class="label">PENDING</span>
{% endif %}