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

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

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

../_images/states_transitions.png

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

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

  • YAML
     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
    
  • XML
     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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="utf-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
    
        <framework:config>
            <framework:workflow name="blog_publishing" type="workflow">
                <framework:marking-store type="single_state">
                  <framework:arguments>currentPlace</framework:arguments>
                </framework:marking-store>
    
                <framework:support>AppBundle\Entity\BlogPost</framework:support>
    
                <framework:place>draft</framework:place>
                <framework:place>review</framework:place>
                <framework:place>rejected</framework:place>
                <framework:place>published</framework:place>
    
                <framework:transition name="to_review">
                    <framework:from>draft</framework:from>
    
                    <framework:to>review</framework:to>
                </framework:transition>
    
                <framework:transition name="publish">
                    <framework:from>review</framework:from>
    
                    <framework:to>published</framework:to>
                </framework:transition>
    
                <framework:transition name="reject">
                    <framework:from>review</framework:from>
    
                    <framework:to>rejected</framework:to>
                </framework:transition>
    
            </framework:workflow>
    
        </framework:config>
    </container>
    
  • 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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // app/config/config.php
    
            $container->loadFromExtension('framework', array(
                // ...
                'workflows' => array(
                    'blog_publishing' => array(
                      'type' => 'workflow', // or 'state_machine'
                      'marking_store' => array(
                        'type' => 'multiple_state', // or 'single_state'
                        'arguments' => array('currentPlace')
                      ),
                      'supports' => array('AppBundle\Entity\BlogPost'),
                      'places' => array(
                        'draft',
                        'review',
                        'rejected',
                        'published',
                      ),
                      'transitions' => array(
                        'to_review'=> array(
                          'from' => 'draft',
                          'to' => 'review',
                        ),
                        'publish'=> array(
                          'from' => 'review',
                          'to' => 'published',
                        ),
                        'reject'=> array(
                          '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()

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

New in version 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, если маркировка данного объекта имеет данное состояние.

New in version 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 %}

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.