Дата обновления перевода 2021-12-25

Рабочий процесс

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

Установка

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

1
$ composer require symfony/workflow

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

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

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

Создание Рабочего процесса

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

_images/states_transitions.png

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

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

  • 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
    26
    27
    28
    # config/packages/workflow.yaml
    framework:
        workflows:
            blog_publishing:
                type: 'workflow' # or '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
    
  • 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
    <!-- config/packages/workflow.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
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <!-- or type="state_machine" -->
            <framework:workflow name="blog_publishing" type="workflow">
                <framework:audit-trail enabled="true"/>
                <framework:marking-store type="single_state">
                    <framework:argument>currentPlace</framework:argument>
                </framework:marking-store>
                <framework:support>App\Entity\BlogPost</framework:support>
                <framework:initial-marking>draft</framework:initial-marking>
                <framework:place>draft</framework:place>
                <framework:place>reviewed</framework:place>
                <framework:place>rejected</framework:place>
                <framework:place>published</framework:place>
                <framework:transition name="to_review">
                    <framework:from>draft</framework:from>
                    <framework:to>reviewed</framework:to>
                </framework:transition>
                <framework:transition name="publish">
                    <framework:from>reviewed</framework:from>
                    <framework:to>published</framework:to>
                </framework:transition>
                <framework:transition name="reject">
                    <framework:from>reviewed</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
    36
    // config/packages/workflow.php
    use App\Entity\BlogPost;
    use Symfony\Config\FrameworkConfig;
    
    return static function (FrameworkConfig $framework) {
        $blogPublishing = $framework->workflows()->workflows('blog_publishing');
        $blogPublishing
            ->type('workflow') // or 'state_machine'
            ->supports([BlogPost::class])
            ->initialMarking(['draft']);
    
        $blogPublishing->auditTrail()->enabled(true);
        $blogPublishing->markingStore()
            ->type('method')
            ->property('currentPlace');
    
        $blogPublishing->place()->name('draft');
        $blogPublishing->place()->name('reviewed');
        $blogPublishing->place()->name('rejected');
        $blogPublishing->place()->name('published');
    
        $blogPublishing->transition()
            ->name('to_review')
                ->from(['draft'])
                ->to(['reviewed']);
    
        $blogPublishing->transition()
            ->name('publish')
                ->from(['reviewed'])
                ->to(['published']);
    
        $blogPublishing->transition()
            ->name('reject')
                ->from(['reviewed'])
                ->to(['rejected']);
    };
    

Tip

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

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

// src/Entity/BlogPost.php
namespace App\Entity;

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

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

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

Note

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

Хранилище маркировки одного состояния использует string для хранения данных. Хранилище маркировки множества состояний использует array для хранения данных.

Tip

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

Tip

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

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

use App\Entity\BlogPost;
use Symfony\Component\Workflow\Exception\LogicException;

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

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

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

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

use App\Entity\BlogPost;
use Symfony\Component\Workflow\WorkflowInterface;

class MyClass
{
    private $blogPublishingWorkflow;

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

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

Как вариант, используйте регистр:

use App\Entity\BlogPost;
use Symfony\Component\Workflow\Registry;

class MyClass
{
    private $workflowRegistry;

    public function __construct(Registry $workflowRegistry)
    {
        $this->workflowRegistry = $workflowRegistry;
    }

    public function toReview(BlogPost $post)
    {
        $blogPublishingWorkflow = $this->workflowRegistry->get($post);

        // ...
    }
}

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]

Вы можете избежать запуска этих событий, используя контекст:

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

New in version 5.1: Константа Workflow::DISABLE_ANNOUNCE_EVENT была представлена в Symfony 5.1.

New in version 5.2: В Symfony 5.2, контекст настраивается для всех событий, кроме событий workflow.guard, которые не будут получать пользовательский $context:

// $context должен быть массивом
$context = ['context_key' => 'context_value'];
$workflow->apply($subject, $transitionName, $context);

// в слушателе событий
$context = $event->getContext(); // returns ['context']

Note

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

Note

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

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

// src/App/EventSubscriber/WorkflowLoggerSubscriber.php
namespace App\EventSubscriber;

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

class WorkflowLoggerSubscriber implements EventSubscriberInterface
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onLeave(Event $event)
    {
        $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()
    {
        return [
            'workflow.blog_publishing.leave' => 'onLeave',
        ];
    }
}

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

$marking = $workflow->apply($post, 'to_review');

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

New in version 5.4: Возможность получать новое значение из маркировки была представлена в Symfony 5.4.

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

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

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

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

// 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)
    {
        /** @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()
    {
        return [
            'workflow.blog_publishing.guard.to_review' => ['guardReview'],
        ];
    }
}

New in version 5.1: Необязательный второй аргумент setBlocked() был представлен в Symfony 5.1.

Выбор событий для запуска

New in version 5.2: Возможность выбирать события для запуска была представлена в Symfony 5.2.

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

  • YAML
     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: []
    
                # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- config/packages/workflow.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 https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <framework:workflow name="blog_publishing">
                <!-- вы можете передать одно или более имен событий -->
                <framework:event-to-dispatch>workflow.leave</framework:event-to-dispatch>
                <framework:event-to-dispatch>workflow.completed</framework:event-to-dispatch>
    
                <!-- передать пустой массив, чтобы не запускать никаких событий -->
                <framework:event-to-dispatch></framework:event-to-dispatch>
    
                <!-- ... -->
            </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
    // config/packages/workflow.php
    use Symfony\Config\FrameworkConfig;
    
    return static function (FrameworkConfig $framework) {
        // ...
    
        $blogPublishing = $framework->workflows()->workflows('blog_publishing');
    
        // ...
        // вы можете передать одно или более имен событий
        $blogPublishing->eventsToDispatch([
            'workflow.leave',
            'workflow.completed',
        ]);
    
        // передать пустой массив, чтобы не запускать никаких событий
        $blogPublishing->eventsToDispatch([]);
    
        // ...
    };
    

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

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 не будет запущено, даже если оно было указано, как событие для запуска для всех переходов в конфигурации рабочего процесса.

New in version 5.1: Константа Workflow::DISABLE_ANNOUNCE_EVENT была представлена в Symfony 5.1.

New in version 5.2: Константы для других событий (ниже) были представлены в Symfony 5.2.

  • 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:

  • YAML
     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
    
  • 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
    <!-- config/packages/workflow.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
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:workflow name="blog_publishing" type="workflow">
    
                <!-- ... предыдущая конфигурация -->
    
                <framework:transition name="to_review">
                    <!-- переход разрешен только, если текущий пользователь имеет роль ROLE_REVIEWER. -->
                    <framework:guard>is_granted("ROLE_REVIEWER")</framework:guard>
                    <framework:from>draft</framework:from>
                    <framework:to>reviewed</framework:to>
                </framework:transition>
    
                <framework:transition name="publish">
                    <!-- или "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted" -->
                    <framework:guard>is_authenticated</framework:guard>
                    <framework:from>reviewed</framework:from>
                    <framework:to>published</framework:to>
                </framework:transition>
    
                <framework:transition name="reject">
                    <!-- или любой валидный язык выражение с "субъектом", ссылающимся на пост -->
                    <framework:guard>is_granted("ROLE_ADMIN") and subject.isStatusReviewed()</framework:guard>
                    <framework:from>reviewed</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
    // config/packages/workflow.php
    use Symfony\Config\FrameworkConfig;
    
    return static function (FrameworkConfig $framework) {
        $blogPublishing = $framework->workflows()->workflows('blog_publishing');
        // ... предыдущая конфигурация
    
        $blogPublishing->transition()
            ->name('to_review')
                // переход разрешен только, если текущий пользователь имеет роль ROLE_REVIEWER.
                ->guard('is_granted("ROLE_REVIEWER")')
                ->from(['draft'])
                ->to(['reviewed']);
    
        $blogPublishing->transition()
            ->name('publish')
                // или "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted"
                ->guard('is_authenticated')
                ->from(['reviewed'])
                ->to(['published']);
    
        $blogPublishing->transition()
            ->name('reject')
                // или любой валидный язык выражение с "субъектом", ссылающимся на пост
                ->guard('is_granted("ROLE_ADMIN") and subject.isStatusReviewed()')
                ->from(['reviewed'])
                ->to(['rejected']);
    };
    

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

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

// 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)
    {
        $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()
    {
        return [
            'workflow.blog_publishing.guard.publish' => ['guardPublish'],
        ];
    }
}

Применение в 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. Эти метаданные могут быть просто заголовком рабочего процесса или очень сложными объектами:

  • 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
    # 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.'
    
  • 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
    <!-- config/packages/workflow.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 https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <framework:workflow name="blog_publishing">
                <framework:metadata>
                    <framework:title>Blog Publishing Workflow</framework:title>
                </framework:metadata>
                <!-- ... -->
                <framework:place name="draft">
                    <framework:metadata>
                        <framework:max-num-of-words>500</framework:max-num-of-words>
                    </framework:metadata>
                </framework:place>
                <!-- ... -->
                <framework:transition name="to_review">
                    <framework:from>draft</framework:from>
                    <framework:to>review</framework:to>
                    <framework:metadata>
                        <framework:priority>0.5</framework:priority>
                    </framework:metadata>
                </framework:transition>
                <framework:transition name="publish">
                    <framework:from>reviewed</framework:from>
                    <framework:to>published</framework:to>
                    <framework:metadata>
                        <framework:hour_limit>20</framework:hour_limit>
                        <framework:explanation>You can not publish after 8 PM.</framework:explanation>
                    </framework:metadata>
                </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
    36
    37
    38
    // config/packages/workflow.php
    use Symfony\Config\FrameworkConfig;
    
    return static function (FrameworkConfig $framework) {
        $blogPublishing = $framework->workflows()->workflows('blog_publishing');
        // ... предыдущая конфигурация
    
        $blogPublishing->metadata([
            'title' => 'Blog Publishing Workflow'
        ]);
    
        // ...
    
        $blogPublishing->place()
            ->name('draft')
            ->metadata([
                'max_num_of_words' => 500,
            ]);
    
        // ...
    
        $blogPublishing->transition()
            ->name('to_review')
                ->from(['draft'])
                ->to(['reviewed'])
                ->metadata([
                    'priority' => 0.5,
                ]);
    
        $blogPublishing->transition()
            ->name('publish')
                ->from(['reviewed'])
                ->to(['published'])
                ->metadata([
                    'hour_limit' => 20,
                    'explanation' => 'You can not publish after 8 PM.',
                ]);
    };
    

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

// src/App/Controller/BlogPostController.php
use App\Entity\BlogPost;
use Symfony\Component\Workflow\WorkflowInterface;
// ...

public function myAction(WorkflowInterface $blogPublishingWorkflow, BlogPost $post)
{
    $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(), который работает со всеми видами метаданных:

// получить "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);

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

// $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>

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