Passo 19: Prendere decisioni con un Workflow

5.0 version
Maintained

Prendere decisioni con un Workflow

Avere uno stato per un modello è abbastanza comune. Lo stato del commento è determinato solo dallo spam checker. E se aggiungessimo altri fattori sui quali prendere delle decisioni?

Potremmo fare in modo che sia l’amministratore del sito a moderare tutti i commenti dopo il controllo dello spam. Il tutto si tradurrebbe in qualcosa di simile:

  • Iniziamo con l’assegnare un stato submitted quando un commento viene inviato da un utente;
  • Lasciamo che lo spam checker analizzi il commento e modifichi lo stato su potential_spam, ham oppure rejected;
  • Se il commento non viene rifiutato dallo spam checker, aspettiamo che sia l’amministratore del sito a decidere se sia buono o meno, modificando lo stato in published oppure rejected.

L’implementazione di questa logica è abbastanza semplice, ma come si può immaginare, all’aumentare del numero delle regole la complessità aumenterà. Invece di codificare noi stessi la logica, possiamo usare il componente Workflow di Symfony:

1
$ symfony composer req workflow

Descrivere i workflow

Il workflow dei commenti può essere descritto nel file 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

Per convalidare il workflow, generiamo una rappresentazione visiva:

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

Nota

Il comando dot fa parte dell’utility Graphviz.

Utilizzare un workflow

Sostituire la logica corrente nel gestore dei messaggi con il 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()]);
+        }
     }
 }

La nuova logica è la seguente:

  • Se la transizione accept è disponibile per il commento nel messaggio, controllare la presenza di spam;
  • A seconda del risultato, scegliere la transizione giusta da applicare;
  • Chiamare apply() per aggiornare il commento tramite una chiamata al metodo setState();
  • Chiamare flush() per salvare le modifiche nel database;
  • Inviare nuovamente il messaggio per avviare la transizione del workflow.

Poiché non abbiamo implementato la validazione da parte dell’amministratore, la prossima volta che il messaggio sarà consumato, verrà generato il seguente messaggio di log: «Dropping comment message».

Implementiamo un’auto-validazione fino al prossimo capitolo:

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

Eseguire symfony server:log e aggiungere un commento nel frontend per vedere tutte le transizioni che si susseguono una dopo l’altra.


  • « Previous Passo 18: Esecuzione asincrona
  • Next » Passo 20: Invio di e-mail agli amministratori

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