Workflow

Дата оновлення перекладу 2024-06-10

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

class BlogPost
{
    // сконфігурована властивіть сховища маркування має бути оголошена
    private $currentPlace;
    private $title;
    private $content;

    // методи гетера/сетера повинні існувати для того, щоб властивість була доступна сховищу маркування
    public function getCurrentPlace(): string
    {
        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');

Використання сховища маркування декількох станів

Якщо ви створюєте workflow, ваше сховище маркування може містити декілька місць одночасно. Ось чому якщо ви використовуєте 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 зберігатиме його значення лише як рядок, що призведе до до втрати поточного місця об'єкта.

Доступ до Workflow у класі

Ви можете використовувати робочий процес всередині класу, використовуючи автомонтування сервісів і 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
use App\Entity\BlogPost;
use Symfony\Component\Workflow\WorkflowInterface;

class MyClass
{
    public function __construct(
    // Symfony впровадить робочий процес 'blog_publishing', сконфігурований раніше
        private WorkflowInterface $blogPublishingWorkflow,
    ) {
    }

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

Щоб отримати увімкнений перехід робочого процесу, ви можете використати метод getEnabledTransition().

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]

Після застосування переходу, оголошення події тестує всі доступні переходи. Це ще раз викличе всі події-охоронці , що може вплинути на продуктивність, якщо вони містять інтенсивну 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()
    {
        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: 'Робочий процес публікації блогу'
            # ...
            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: 'Ви не можете публікувати після 8ої вечора.'

Потім ви можете отримати доступ до цих матадних у вашому контролері наступним чином:

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', "Ви успішно застосувалли перехід з заголовком: '$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>