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();
Также можно прослушивать эти события, объявив слушателей событий со следующими атрибутами:
- AsAnnounceListener
- AsCompletedListener
- AsEnterListener
- AsEnteredListener
- AsGuardListener
- AsLeaveListener
- AsTransitionListener
Эти атрибуты работают как атрибуты 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>