Як використовувати Workflow

Як використовувати Workflow

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

Набір місць і переходів створює визначення. Робочому потоку необхідно Definition (визначення) і спосіб написати стани об'єктів (наприклад, сутність класу MarkingStoreInterface).

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

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' # або 'state_machine'
            marking_store:
                type: 'multiple_state' # або '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

// Оновити currentState поста
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.

Для захисних подій існує розширений клас GuardEvent. Цей клас має ще два методи:

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