Passo 19: Tomando Decisões com um Workflow

5.0 version
Maintained

Tomando Decisões com um Workflow

Ter um estado para um modelo é bastante comum. O estado do comentário é determinado apenas pelo verificador de spam. E se adicionarmos mais fatores de decisão?

Nós podemos querer deixar o administrador do site moderar todos os comentários após o verificador de spam. O processo seria algo como:

  • Comece com um estado submitted quando um comentário é enviado por um usuário;
  • Deixe o verificador de spam analisar o comentário e mudar o estado para potential_spam, ham ou rejected;
  • Se não for rejeitado, aguarde o administrador do site decidir se o comentário é bom o suficiente, mudando o estado para published ou rejected.

Implementar essa lógica não é muito complexo, mas você pode imaginar que adicionar mais regras aumentaria bastante a complexidade. Em vez de programar a lógica, podemos usar o Componente Workflow do Symfony:

1
$ symfony composer req workflow

Descrevendo Workflows

O workflow dos comentários pode ser descrito no arquivo 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

Para validar o workflow, gere uma representação visual:

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

Nota

O comando dot faz parte do utilitário Graphviz.

Usando um Workflow

Substitua a lógica atual no manipulador de mensagens pelo workflow:

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()]);
+        }
     }
 }

A nova lógica diz o seguinte:

  • Se a transição accept estiver disponível para o comentário na mensagem, verifique se há spam;
  • Dependendo do resultado, escolha a transição correta a ser aplicada;
  • Chame apply() para atualizar o Comment através de uma chamada ao método setState();
  • Chame flush() para fazer o commit das alterações no banco de dados;
  • Reenvie a mensagem para permitir que o workflow faça a transição novamente.

Como não implementamos a validação do administrador, na próxima vez que a mensagem for consumida, a mensagem “Dropping comment message” será registrada no log.

Vamos implementar uma validação automática até o próximo capítulo:

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()]);
         }

Execute symfony server:log e adicione um comentário no frontend para ver todas as transições acontecendo uma após a outra.


  • « Previous Passo 18: Tornando Assíncrono
  • Next » Passo 20: Enviando E-Mails aos Administradores

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