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
oppurerejected
; - 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
oppurerejected
.
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
:
Per convalidare il workflow, generiamo una rappresentazione visiva:
1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
Note
Il comando dot
fa parte dell'utility Graphviz.
Utilizzare un workflow
Sostituire la logica corrente nel gestore dei messaggi con il workflow:
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');
- }
- $this->entityManager->flush();
+ 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->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 metodosetState()
; - 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:
1 2 3 4 5 6 7 8 9 10 11 12
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -50,6 +50,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.
Trovare servizi nel Dependency Injection Container
Quando si utilizza la dependency injection, otteniamo i servizi dal dependency injection container attraverso il type hinting di un'interfaccia, oppure alcune volte attraverso il nome di una classe che rappresenta una implementazione concreta dell'interfaccia. Ma quando un'interfaccia ha più di una implementazione concreta, Symfony non può indovinare quale implementazione utilizzare. Abbiamo bisogno di un modo per essere espliciti.
Ci siamo appena imbattuti, nella sezione precedente, in un esempio in cui abbiamo iniettato l'interfaccia WorkflowInterface
.
Sicome stiamo iniettando l'interfaccia generica WorkflowInterface
nel costruttore, come può Symfony indovinare quale implementazione del workflow utilizzare? Symfony usa una convenzione basata sul nome dell'argomento: $commentStateMachine
si riferisce alla variabile di configurazione del workflow comment
(il cui tipo è state_machine
). Provate con un altro argomento e fallirà.
Se non si ricorda la convezione, usare il comando debug::container
. Cerchiamo tutti i servizi che contengono "workflow":
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$ symfony console debug:container workflow
Select one of the following services to display its information:
[0] console.command.workflow_dump
[1] workflow.abstract
[2] workflow.marking_store.method
[3] workflow.registry
[4] workflow.security.expression_language
[5] workflow.twig_extension
[6] monolog.logger.workflow
[7] Symfony\Component\Workflow\Registry
[8] Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
[9] Psr\Log\LoggerInterface $workflowLogger
>
Si noti la scelta 8
, Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
, che dice che l'uso di $commentStateMachine
come nome di parametro ha un significato speciale.
Note
Avremmo potuto usare il comando debug:autowiring
come visto in un capitolo precedente:
1
$ symfony console debug:autowiring workflow
Andare oltre
- Documentazione su Workflows e State Machine e quando scegliere una o l'altra;
- Documentazione su Symfony Workflow.