Workflow

Дата обновления перевода 2024-08-01

Workflow

Использование компонента Workflow внутри приложения Symfony требует для начала знаний базовой теории и концептов рабочих процессов и машин состояний. Прочтите эту статью, чтобы получить краткое представление.

Установка

В приложениях, использующих Symfony Flex , выполните эту команду, чтобы установить функцию рабочего процесса, перед ее использованием:

1
$ composer require symfony/workflow

Конфигурация

Чтобы увидеть все опции конфигурации, если вы используете компонент внутри проекта Symfony, выполните эту команду:

1
$ php bin/console config:dump-reference framework workflows

Создание Workflow

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

Пример диаграммы состояний для рабочего процесса с указанием переходов и мест.

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

Рассмотрите следующий пример для поста блога. Пост может иметь такие места: draft, reviewed, rejected, published. Вы можете определить рабочий процесс таким образом:

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
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            type: 'workflow' # или 'state_machine'
            audit_trail:
                enabled: true
            marking_store:
                type: 'method'
                property: 'currentPlace'
            supports:
                - App\Entity\BlogPost
            initial_marking: draft
            places:          # определение мест вручную является необязательным
                - draft
                - reviewed
                - rejected
                - published
            transitions:
                to_review:
                    from: draft
                    to:   reviewed
                publish:
                    from: reviewed
                    to:   published
                reject:
                    from: reviewed
                    to:   rejected

Tip

Если вы создаете ваши первые рабочие процессы, подумайте об использовании команды workflow:dump, чтобы отладить содержание рабочего процесса.

Tip

Вы можете использовать PHP-константы в YAML-файлах с помощью нотации !php/const. Например, вы можете использовать !php/const App\Entity\BlogPost::STATE_DRAFT вместо 'draft' или !php/const App\Entity\BlogPost::TRANSITION_TO_REVIEW вместо 'to_review'.

Tip

Опцию places можно опустить, если ваши переходы определяют все места, которые используются в рабочем процессе. Symfony автоматически извлечет места из переходов.

7.1

Поддержка пропуска опции places была представлена в Symfony 7.1.

Сконфигурированное свойство будет использовано через его реализованные методы геттера/сеттера хранилищем маркировки:

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
// src/Entity/BlogPost.php
namespace App\Entity;

class BlogPost
{
    // сконфигурированное свойство хранилища маркировки должно быть объявлено
    private $currentPlace;
    private $title;
    private $content;

    // методы геттера/сеттера должны существовать для того, чтобы свойство было доступно хранилищу маркировки
    public function getCurrentPlace()
    {
        return $this->currentPlace;
    }

    public function setCurrentPlace(string $currentPlace, array $context = []): void
    {
        $this->currentPlace = $currentPlace;
    }
}


    // вам не нужно устанавливать начальную маркировку в конструкторе или любом другом методе;
    // это конфигурируется в рабочем процессе с помощью опции 'initial_marking'
}

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

1
2
3
4
5
6
7
8
9
10
// src/Entity/BlogPost.php
namespace App\Entity;

class BlogPost
{
    // сконфигурированное свойство хранилища маркировки должно быть объявлено
    public string $currentPlace;
    public string $title;
    public string $content;
}

При использовании публичных свойств контекст не поддерживается. Для того чтобы его поддерживать, вы должны объявить сеттер для записи в вашем свойстве:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Entity/BlogPost.php
namespace App\Entity;

class BlogPost
{
    public string $currentPlace;
    // ...

    public function setCurrentPlace(string $currentPlace, array $context = []): void
    {
        // присвоить свойство и что-то сделать с контекстом
    }
}

Note

Тип хранилища маркировки может быть "multiple_state" или "single_state". Хранилище маркировки одного состояния не поддерживает модель, расположенную в нескольких местах одномоментно. Это означает, что "workflow" должен использовать хранилище маркировки "multiple_state", а "state_machine" должна использовать хранилище маркировки "single_state". Symfony конфигурирует хранилище маркировки в соответствии с "type" по умолчанию, поэтому лучше его не конфигурировать.

Хранилище маркировки одного состояния использует string для хранения данных. Хранилище маркировки множества состояний использует array для хранения данных. Если хранилище маркировки состояний не определено, то в обоих случаях нужно возвращать null (например, пример выше должен определять тип возврата вроде App\Entity\BlogPost::getCurrentPlace(): ?array или вроде App\Entity\BlogPost::getCurrentPlace(): ?string).

Tip

Атрибуты marking_store.type (значение по умолчанию зависит от значения type) и property (значение по умолчанию ['marking']) опции marking_store - не обязательны. Если их опустить, будут использованы их значения по умолчанию. Очень рекомендуется использовать значение по умолчанию.

Tip

Установка опции audit_trail.enabled как true заставляет приложение генерировать детализированные сообщения логов для активности рабочег процесса.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use App\Entity\BlogPost;
use Symfony\Component\Workflow\Exception\LogicException;

$post = new BlogPost();
// вам не нужно устанавливать начальную маркировку с помощью кода; это конфигурируется
// в рабочем процессе с помощью опции 'initial_marking'

$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 $exception) {
    // ...
}

// Увидеть все доступные переходы для поста в текущем состоянии
$transitions = $workflow->getEnabledTransitions($post);
// Увидеть конкретный доступный переход для поста в текущем состоянии
$transition = $workflow->getEnabledTransition($post, 'publish');

Использование хранилищ маркировки с несколькими состояниями

Если вы создаете рабочий процесс, ваше хранилище маркировки может содержать несколько мест одновременно. Вот почему, если вы используете Doctrine, определение столбца должно использовать тип json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Entity/BlogPost.php
namespace App\Entity;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class BlogPost
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private int $id;

    #[ORM\Column(type: Types::JSON)]
    private array $currentPlaces;

    // ...
}

Caution

Не следует использовать тип simple_array для хранилища маркировки. Внутри хранилища маркировки с несколькими состояниями, места хранятся как ключи с одним значением, например, ['draft' => 1]. Если хранилище маркировки содержит только одно место, этот тип Doctrine будет хранить его значение только как строку, что приведет к потере текущего места объекта.

Доступ к рабочему процессу в классе

Вы можете использовать рабочий процесс внутри класс, используя автомонтирование сервисов и camelCased workflow name + Workflow в качестве имени параметра. Если это тип машины состояний, используйте camelCased workflow name + StateMachine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use App\Entity\BlogPost;
use Symfony\Component\Workflow\WorkflowInterface;

class MyClass
{
    public function __construct(
    // Symfony внедрит рабочий процесс 'blog_publishing', сконфигурированный ранее
    public function __construct(WorkflowInterface $blogPublishingWorkflow)
    {
        $this->blogPublishingWorkflow = $blogPublishingWorkflow;
    }

    public function toReview(BlogPost $post): void
    {
        // Обновить currentState поста
        try {
            $this->blogPublishingWorkflow->apply($post, 'to_review');
        } catch (LogicException $exception) {
            // ...
        }
        // ...
    }
}

7.1

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

Рабочие процессы также могут быть внедрены, благодаря их имени и атрибуту Target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use App\Entity\BlogPost;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\Workflow\WorkflowInterface;

class MyClass
{
    public function __construct(
        #[Target('blog_publishing')]
        private WorkflowInterface $workflow
    ) {
    }

    // ...
}

Это позволяет вам декоррелировать имя аргумента и имя любой реализации.

Tip

Если вы хотите извлечь все рабочие процеси, например, в целях документации, вы можете внедрить все сервисы, используя следующий тег:

  • workflow: все рабочие процессы и машина состояний;
  • workflow.workflow: все рабочие процессы;
  • workflow.state_machine: все машины состояний.

Обратите внимание, что метаданные рабочего процесса прикрепляются к тегам под ключом metadata, предоставляя в ваше распоряжение больше контекста и информации о рабочем процессе. Подробнее об атрибутах тегов и хранении метаданных рабочего процесса .

7.1

Возможность прикрепления конфигурации к тегу была представлена в Symfony 7.1.

Tip

Вы можете найти список доступных сервисов рабочего процесса с помощью команды php bin/console debug:autowiring workflow.

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

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

Каждый шаг имеет три события, которые запускаются по порядку:

  • Событие для всех рабочих процессов;
  • Событие для задействованного рабочего процесса;
  • Событие для задействованного рабочего процесса с конкретным переходом или именем места.

Когда инициируется переход состояния, события запускаются в следующем порядке:

workflow.guard

Валидирует, блокируется ли переход (см. события-охранники и блокировка переходов ).

Три запускающихся события:

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

Субъект вот-вот покинет место.

Три запускающихся события:

  • workflow.leave
  • workflow.[workflow name].leave
  • workflow.[workflow name].leave.[place name]
workflow.transition

Субъект проходит переход.

Три запускающихся события:

  • workflow.transition
  • workflow.[workflow name].transition
  • workflow.[workflow name].transition.[transition name]
workflow.enter

Субъект вот-вот войдет в новое место. Это событие запускается прямо перед тем, как обновляются места субъекта, что означает, что маркировка субъекта еще не обновлена в соответствии с новыми местами.

Три запускающихся события:

  • workflow.enter
  • workflow.[workflow name].enter
  • workflow.[workflow name].enter.[place name]
workflow.entered

Субъект вошел в места и маркировка обновилась.

Три запускающихся события:

  • workflow.entered
  • workflow.[workflow name].entered
  • workflow.[workflow name].entered.[place name]
workflow.completed

Объект выполнил этот переход.

Три запускающихся события:

  • workflow.completed
  • workflow.[workflow name].completed
  • workflow.[workflow name].completed.[transition name]
workflow.announce

Запускается для каждого перехода, который теперь доступен субъекту.

Три запускающихся события:

  • workflow.announce
  • workflow.[workflow name].announce
  • workflow.[workflow name].announce.[transition name]

После применения перехода, анонсированное событие тестирует все доступные переходы. Это еще раз вызовет все события-охранники events , что может повлиять на производительность, если они содержат интенсивный CPU или нагрузку базы данных.

Если вам не нужно оглашать событие, отключите его, используя контекст:

1
$workflow->apply($subject, $transitionName, [Workflow::DISABLE_ANNOUNCE_EVENT => true]);

Note

Выходы и входы в события запускаются даже для переходов, которые остаются в одном месте.

Note

Если вы инициализируете маркировку, вызвав $workflow->getMarking($object);, то событие workflow.[workflow_name].entered.[initial_place_name] будет вызвано с контекстом по умолчанию (Workflow::DEFAULT_INITIAL_CONTEXT).

Вот пример того, как включить логирование для каждого раза, когда рабочий процесс "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
30
31
32
33
34
35
// src/App/EventSubscriber/WorkflowLoggerSubscriber.php
namespace App\EventSubscriber;

use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\LeaveEvent;

class WorkflowLoggerSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function onLeave(Event $event): void
    {
        $this->logger->alert(sprintf(
            'Blog post (id: "%s") performed transition "%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(): array
    {
        return [
            LeaveEvent::getName('blog_publishing') => 'onLeave',
            // если хотите, можете написать имя события вручную, как здесь:
            // 'workflow.blog_publishing.leave' => 'onLeave',
        ];
    }
}

Tip

Все встроенные события рабочего процесса определяют метод getName(?string $workflowName, ?string $transitionOrPlaceName) для создания полного имени события без необходимости работать со строками. Вы также можете использовать этот метод в своих пользовательских событиях через
EventNameTrait.

7.1

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

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

1
2
3
4
$marking = $workflow->apply($post, 'to_review');

// содержит новое значение
$marking->getContext();

Также можно прослушивать эти события, объявив слушателей событий со следующими атрибутами:

Эти атрибуты работают как атрибуты AsEventListener:

1
2
3
4
5
6
7
8
9
10
class ArticleWorkflowEventListener
{
    #[AsTransitionListener(workflow: 'my-workflow', transition: 'published')]
    public function onPublishedTransition(TransitionEvent $event): void
    {
        // ...
    }

    // ...
}

Вы можете обратиться к документации об определении слушателей событий с помощью PHP-атрибутов . для дальнейшего использования.

События-охранники

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

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

Этот пример останавливает любой пост блога от перехода в "reviewed", если у него нет заголовка:

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
// src/App/EventSubscriber/BlogPostReviewSubscriber.php
namespace App\EventSubscriber;

use App\Entity\BlogPost;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\GuardEvent;

class BlogPostReviewSubscriber implements EventSubscriberInterface
{
    public function guardReview(GuardEvent $event): void
    {
        /** @var BlogPost $post */
        $post = $event->getSubject();
        $title = $post->title;

        if (empty($title)) {
            $event->setBlocked(true, 'This blog post cannot be marked as reviewed because it has no title.');
        }
    }

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

Выбор событий для развёртывания

Если вы предпочитаете контролировать, какие события разворачиваются при выполнении каждого перехода, используйте опцию конфигурации events_to_dispatch. Эта опция не применяется к событиям-охранникам , которые запускаются всегда:

1
2
3
4
5
6
7
8
9
10
11
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            # вы можете передать одно или более имен событий
            events_to_dispatch: ['workflow.leave', 'workflow.completed']

            # передать пустой массив, чтобы не запускать никаких событий
            events_to_dispatch: []

            # ...

Вы также можете отключить конкретное событие от развёртывания при применении перехода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use App\Entity\BlogPost;
use Symfony\Component\Workflow\Exception\LogicException;

$post = new BlogPost();

$workflow = $this->container->get('workflow.blog_publishing');

try {
    $workflow->apply($post, 'to_review', [
        Workflow::DISABLE_ANNOUNCE_EVENT => true,
        Workflow::DISABLE_LEAVE_EVENT => true,
    ]);
} catch (LogicException $exception) {
    // ...
}

Отключение события для конкретного перехода будет главенствовать над любыми указанными в конфигурации рабочего процесса событиями. В примере выше, событие workflow.leave не будет запущено, даже если оно было указано, как событие для развёртывания для всех переходов в конфигурации рабочего процесса.

Вот все доступные константы:

  • Workflow::DISABLE_LEAVE_EVENT
  • Workflow::DISABLE_TRANSITION_EVENT
  • Workflow::DISABLE_ENTER_EVENT
  • Workflow::DISABLE_ENTERED_EVENT
  • Workflow::DISABLE_COMPLETED_EVENT

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

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

getMarking()
Возвращает Marking рабочего процесса.
getSubject()
Возвращает объект, запускающий событие.
getTransition()
Возвращает Transition, который запускает событие.
getWorkflowName()
Возвращает строку с именем рабочего процесса, вызвавшего событие.
getMetadata()
Возвращает метаданные.

Для событий-охранников, существует расширенный класс GuardEvent. Этот класс имеет такие дополнительные методы:

isBlocked()
Вовзращается, если переход заблокирован.
setBlocked()
Устанавливает заблокированное значение.
getTransitionBlockerList()
Возвращает событие TransitionBlockerList. См. блокировка переходов .
addTransitionBlocker()
Добавляет экземпляр TransitionBlocker.

Блокировка переходов

Выполнение рабочего процесса можно контролировать, вызывая пользовательскую логику, чтобы решить, блокировать ли текущий переход, перед его применением. Эта функция предоставлена "охранниками", что может быть использовано двумя способами.

Во-первых, вы можете слушать события-охранники . Как вариант, вы можете определить опцию конфигурации guard для перехода. Значение этой опции - любое валидное выражение, созданное с помощью компонента ExpressionLanguage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            # предыдущая конфигурация
            transitions:
                to_review:
                    # переход разрешен только, если текущий пользователь имеет роль ROLE_REVIEWER.
                    guard: "is_granted('ROLE_REVIEWER')"
                    from: draft
                    to:   reviewed
                publish:
                    # или "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid"
                    guard: "is_authenticated"
                    from: reviewed
                    to:   published
                reject:
                    # или любой валидный язык выражение с "субъектом", ссылающимся на поддерживаемый объект
                    guard: "is_granted('ROLE_ADMIN') and subject.isRejectable()"
                    from: reviewed
                    to:   rejected

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

Этот пример был упрощен; в производстве вам лучше использовать компонент Translation, чтобы управлять сообщениями в одном месте:

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
// src/App/EventSubscriber/BlogPostPublishSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\TransitionBlocker;

class BlogPostPublishSubscriber implements EventSubscriberInterface
{
    public function guardPublish(GuardEvent $event): void
    {
        $eventTransition = $event->getTransition();
        $hourLimit = $event->getMetadata('hour_limit', $eventTransition);

        if (date('H') <= $hourLimit) {
            return;
        }

        // Заблокировать переход "publish", если уже позже 8 вечера
        // с сообщением для конечного пользователя
        $explanation = $event->getMetadata('explanation', $eventTransition);
        $event->addTransitionBlocker(new TransitionBlocker($explanation , '0'));
    }

    public static function getSubscribedEvents(): array
    {
        return [
            'workflow.blog_publishing.guard.publish' => ['guardPublish'],
        ];
    }
}

Создания вашего собственного хранилища маркировки

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace App\Workflow\MarkingStore;

use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;

final class BlogPostMarkingStore implements MarkingStoreInterface
{
    public function getMarking(BlogPost $subject): Marking
    {
        return new Marking([$subject->getCurrentPlace() => 1]);
    }

    public function setMarking(BlogPost $subject, Marking $marking): void
    {
        $marking = key($marking->getPlaces());
        $subject->setCurrentPlace($marking);
    }
}

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

1
2
3
4
5
6
7
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            # ...
            marking_store:
                service: 'App\Workflow\MarkingStore\BlogPostMarkingStore'

Применение в Twig

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

workflow_can()
Возвращает true, если заданный объект может совершать заданный переход.
workflow_transitions()
Возвращает массив со всеми переходами, включенными для заданного объекта.
workflow_transition()
Возвращает конкретный переход, включенный для заданного объекта, и имя перехода.
workflow_marked_places()
Возвращает массив с именами мест заданной маркировки.
workflow_has_marked_place()
Возвращает true, если маркировка заданного объекта имеет заданное состояние.
workflow_transition_blockers()
Возвращает TransitionBlockerList для заданного перехода.

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

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
<h3>Actions on Blog Post</h3>
{% if workflow_can(post, 'publish') %}
    <a href="...">Publish</a>
{% endif %}
{% if workflow_can(post, 'to_review') %}
    <a href="...">Submit to review</a>
{% endif %}
{% if workflow_can(post, 'reject') %}
    <a href="...">Reject</a>
{% endif %}

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

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

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

{# Закольцевать блокировщики переходов #}
{% for blocker in workflow_transition_blockers(post, 'publish') %}
    <span class="error">{{ blocker.message }}</span>
{% endfor %}

Хранение метаданных

Если вам нужно, вы можете хранить произвольные метаданные в рабочих процессах, их мстах и переходах, используя опцию metadata. Эти метаданные могут быть просто заголовком рабочего процесса или очень сложными объектами:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            metadata:
                title: 'Blog Publishing Workflow'
            # ...
            places:
                draft:
                    metadata:
                        max_num_of_words: 500
                # ...
            transitions:
                to_review:
                    from: draft
                    to:   review
                    metadata:
                        priority: 0.5
                publish:
                    from: reviewed
                    to:   published
                    metadata:
                        hour_limit: 20
                        explanation: 'You can not publish after 8 PM.'

Затем вы можете получить доступ к этим метаданным в вашем контроллере следующим образом:

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
// src/App/Controller/BlogPostController.php
use App\Entity\BlogPost;
use Symfony\Component\Workflow\WorkflowInterface;
// ...

public function myAction(WorkflowInterface $blogPublishingWorkflow, BlogPost $post): Response
{
    $title = $blogPublishingWorkflow
        ->getMetadataStore()
        ->getWorkflowMetadata()['title'] ?? 'Default title'
    ;

    $maxNumOfWords = $blogPublishingWorkflow
        ->getMetadataStore()
        ->getPlaceMetadata('draft')['max_num_of_words'] ?? 500
    ;

    $aTransition = $blogPublishingWorkflow->getDefinition()->getTransitions()[0];
    $priority = $blogPublishingWorkflow
        ->getMetadataStore()
        ->getTransitionMetadata($aTransition)['priority'] ?? 0
    ;

    // ...
}

Существует метод getMetadata(), который работает со всеми видами метаданных:

1
2
3
4
5
6
7
8
// получить "workflow metadata", передавая ключ метаданных, как аргумент
$title = $workflow->getMetadataStore()->getMetadata('title');

// получить "place metadata", передавая ключ метаданных, как первый аргумент, а имя места, как второй
$maxNumOfWords = $workflow->getMetadataStore()->getMetadata('max_num_of_words', 'draft');

// получить "transition metadata", передавая ключ метаданных, как первый аргумент, а объект Перехода, как второй
$priority = $workflow->getMetadataStore()->getMetadata('priority', $aTransition);

В флеш-сообщении в вашем контроллере:

1
2
3
4
5
// $transition = ...; (an instance of Transition)

// $workflow - это экземпляр Рабочего процесса, излвеченный из Регистра, или внедренный напрямую (см. выше)
$title = $workflow->getMetadataStore()->getMetadata('title', $transition);
$this->addFlash('info', "You have successfully applied the transition with title: '$title'");

Доступ к метаданным также можно получить в слушателе, из объекта Event.

В шаблонах Twig, метаданные доступны через функцию workflow_metadata():

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
<h2>Metadata of Blog Post</h2>
<p>
    <strong>Workflow</strong>:<br>
    <code>{{ workflow_metadata(blog_post, 'title') }}</code>
</p>
<p>
    <strong>Current place(s)</strong>
    <ul>
        {% for place in workflow_marked_places(blog_post) %}
            <li>
                {{ place }}:
                <code>{{ workflow_metadata(blog_post, 'max_num_of_words', place) ?: 'Unlimited'}}</code>
            </li>
        {% endfor %}
    </ul>
</p>
<p>
    <strong>Enabled transition(s)</strong>
    <ul>
        {% for transition in workflow_transitions(blog_post) %}
            <li>
                {{ transition.name }}:
                <code>{{ workflow_metadata(blog_post, 'priority', transition) ?: 0 }}</code>
            </li>
        {% endfor %}
    </ul>
</p>
<p>
    <strong>to_review Priority</strong>
    <ul>
        <li>
            to_review:
            <code>{{ workflow_metadata(blog_post, 'priority', workflow_transition(blog_post, 'to_review')) }}</code>
        </li>
    </ul>
</p>