Шаг 19: Управление состоянием с помощью Workflow

5.0 version
Maintained

Управление состоянием с помощью Workflow

Наличие какого-либо состояния у модели — довольно обычное явление. Состояние комментария определяет только антиспам-сервис. Но что, если в будущем у вас появятся больше факторов для изменения состояния?

Возможно, вы захотите дать администратору сайта возможность модерировать все комментарии после того, как они будут проверены антиспам-сервисом. Вот как будет выглядеть этот процесс:

  • Начинаем с состояния submitted, когда пользователь отправляет комментарий;
  • Делегируем антиспам-сервису проанализировать комментарий и переключим его в зависимости от результата в одно из состояний: potential_spam, ham или rejected;
  • Если комментарий не был отклонён (то есть он не спам), ожидаем, пока администратор не решит, достаточно ли комментарий хорош, изменив его состояние на published или rejected.

Реализация данной логики — не слишком сложная задача. Однако добавление дополнительных правил значительно усложнит эту задачу. Воспользуемся Symfony-компонентом Workflow, чтобы не писать самим логику с нуля:

1
$ symfony composer req workflow

Определение бизнес-процессов

Бизнес-процесс комментария можно описать в конфигурационном файле config/packages/workflow.yaml:

config/packages/workflow.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
29
30
31
32
33
34
35
36
37
38
39
40
41
framework:
    workflows:
        comment:
            type: state_machine
            audit_trail:
                enabled: "%kernel.debug%"
            marking_store:
                type: 'method'
                property: 'state'
            supports:
                - App\Entity\Comment
            initial_marking: submitted
            places:
                - submitted
                - ham
                - potential_spam
                - spam
                - rejected
                - published
            transitions:
                accept:
                    from: submitted
                    to:   ham
                might_be_spam:
                    from: submitted
                    to:   potential_spam
                reject_spam:
                    from: submitted
                    to:   spam
                publish:
                    from: potential_spam
                    to:   published
                reject:
                    from: potential_spam
                    to:   rejected
                publish_ham:
                    from: ham
                    to:   published
                reject_ham:
                    from: ham
                    to:   rejected

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

1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
../_images/workflow.png

Примечание

Команда dot является частью утилиты Graphviz.

Использование бизнес-процессов

Замените текущую логику в обработчике сообщений на новую с использованием определенного ранее бизнес-процесса:

patch_file
 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -6,19 +6,28 @@ use App\Message\CommentMessage;
 use App\Repository\CommentRepository;
 use App\SpamChecker;
 use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
 use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Workflow\WorkflowInterface;

 class CommentMessageHandler implements MessageHandlerInterface
 {
     private $spamChecker;
     private $entityManager;
     private $commentRepository;
+    private $bus;
+    private $workflow;
+    private $logger;

-    public function __construct(EntityManagerInterface $entityManager, SpamChecker $spamChecker, CommentRepository $commentRepository)
+    public function __construct(EntityManagerInterface $entityManager, SpamChecker $spamChecker, CommentRepository $commentRepository, MessageBusInterface $bus, WorkflowInterface $commentStateMachine, LoggerInterface $logger = null)
     {
         $this->entityManager = $entityManager;
         $this->spamChecker = $spamChecker;
         $this->commentRepository = $commentRepository;
+        $this->bus = $bus;
+        $this->workflow = $commentStateMachine;
+        $this->logger = $logger;
     }

     public function __invoke(CommentMessage $message)
@@ -28,12 +37,21 @@ class CommentMessageHandler implements MessageHandlerInterface
             return;
         }

-        if (2 === $this->spamChecker->getSpamScore($comment, $message->getContext())) {
-            $comment->setState('spam');
-        } else {
-            $comment->setState('published');
-        }
+
+        if ($this->workflow->can($comment, 'accept')) {
+            $score = $this->spamChecker->getSpamScore($comment, $message->getContext());
+            $transition = 'accept';
+            if (2 === $score) {
+                $transition = 'reject_spam';
+            } elseif (1 === $score) {
+                $transition = 'might_be_spam';
+            }
+            $this->workflow->apply($comment, $transition);
+            $this->entityManager->flush();

-        $this->entityManager->flush();
+            $this->bus->dispatch($message);
+        } elseif ($this->logger) {
+            $this->logger->debug('Dropping comment message', ['comment' => $comment->getId(), 'state' => $comment->getState()]);
+        }
     }
 }

Новая логика выглядит следующим образом:

  • Если комментарий может перейти в состояние accept, значит проверяем сообщение на спам;
  • В зависимости от результата проверки, нужно выбрать подходящий переход;
  • Вызываем метод apply(), чтобы обновить состояние для объекта Comment, который в свою очередь вызывает в этом объекте метод setState();
  • Сохраняем данные в базе данных, используя метод flush();
  • Повторно отправляем сообщение на шину, чтобы ещё раз запустить бизнес-процесс комментария для определения следующего перехода.

Так как ещё не реализована возможность проверки сообщения администратором, при следующий обработке сообщения в лог запишется следующее: «Dropping comment message».

Перед тем как начать следующую главу, давайте добавим автоматическую проверку:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -47,6 +47,9 @@ class CommentMessageHandler implements MessageHandlerInterface
             $this->entityManager->flush();

             $this->bus->dispatch($message);
+        } elseif ($this->workflow->can($comment, 'publish') || $this->workflow->can($comment, 'publish_ham')) {
+            $this->workflow->apply($comment, $this->workflow->can($comment, 'publish') ? 'publish' : 'publish_ham');
+            $this->entityManager->flush();
         } elseif ($this->logger) {
             $this->logger->debug('Dropping comment message', ['comment' => $comment->getId(), 'state' => $comment->getState()]);
         }

Выполните команду symfony server:log и добавьте комментарий к любой конференции, чтобы увидеть в терминале, как один за другим происходят переходы состояний.


  • « Previous Шаг 18: Переход к асинхронности
  • Next » Шаг 20: Отправка электронной почты администраторам

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.