步骤 19: 用 Workflow 进行决策

5.0 version
Maintained

用 Workflow 进行决策

让模型拥有状态是很常见的。目前评论的状态只由垃圾信息检查器来决定。如果我们要加入更多的决策因素,那该怎么做?

在垃圾信息检查器判别之后,我们可能想要让网站管理员来管理所有评论。这个流程看上去可能是这样的:

  • 当用户提交评论时,我们把它的初始状态设为 submitted
  • 然后垃圾信息检查器来分析这条评论,把它的状态转为 potential_spamhamrejected 中的一个;
  • 如果评论不是 rejected 状态,那需要等待管理员根据它的内容质量来决定,是把它切换到 published 还是 rejected

实现这个逻辑并不复杂,但你可以想到,不断增加类似的规则会大幅提高复杂度。我们可以使用 Symfony 的 Workflow 组件,而不是自己实现它。

1
$ symfony composer req workflow

描述工作流(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() 方法来更新评论,它会调用评论的 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,然后在前端页面添加一个评论,看一下输出的一个个状态迁移。


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