Beslissingen nemen door middel van een workflow
Het hebben van een state voor een model is vrij gebruikelijk. De state van een reactie wordt alleen bepaald door de spam checker. Wat als we meer beslissingsfactoren toevoegen?
We willen een websitebeheerder misschien wel de reacties laten modereren, na het checken op spam. Het proces zou er dan als volgt uit zien:
- We beginnen met een
submitted
state wanneer een reactie wordt ingediend door een gebruiker; - Laat de spamchecker de reactie analyseren en zet de status om naar
potential_spam
,ham
ofrejected
; - Indien niet afgewezen, wacht dan tot de websitebeheerder beslist of de reactie goed genoeg is door de staat op
published
ofrejected
te zetten.
Het implementeren van deze logica is niet al te complex, maar je kan je voorstellen dat het toevoegen van meer regels de complexiteit aanzienlijk kan vergroten. In plaats van zelf de logica te coderen, kunnen we het Symfony Workflow-component gebruiken:
1
$ symfony composer req workflow
Workflows beschrijven
De reactie-workflow kan in het config/packages/workflow.yaml
bestand worden beschreven:
Om de workflow te valideren, genereer je een visuele weergave:
1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
Note
Het dot
command is een onderdeel van de Graphviz utility.
Een workflow gebruiken
Vervang de huidige logica in de message-handler door de 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()]);
+ }
}
}
De nieuwe logica luidt als volgt:
- Als de
accept
-overgang beschikbaar is voor de reactie in het bericht, controleer dan op spam; - Kies, afhankelijk van de uitkomst, de juiste overgang om toe te passen;
- Roep
apply()
aan, om de reactie bij te werken via een aanroep op desetState()
methode; - Roep
flush()
aan om de wijzigingen in de database vast te leggen; - Verstuur het bericht opnieuw om de workflow opnieuw toe te passen.
Aangezien we de adminvalidatie niet hebben geïmplementeerd, zal de volgende keer dat het bericht wordt verwerkt, "Dropping comment message" worden gelogd.
Laten we een automatische validatie implementeren tot aan het volgende hoofdstuk:
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()]);
}
Draai symfony server:log
en voeg een reactie toe in de frontend om alle overgangen achter elkaar te zien gebeuren.
Services zoeken in de Dependency Injection-container
Bij het gebruik van dependency injection krijgen we services van de dependency injection container door een interface of soms een concrete implementatie-class-naam te typen. Maar als een interface meerdere implementaties heeft, kan Symfony niet raden welke jij nodig hebt. We hebben een manier nodig om expliciet te zijn.
We zijn zojuist zo'n voorbeeld tegengekomen met de injectie van een 'WorkflowInterface' in de vorige paragraaf.
Hoe kan Symfony raden welke workflow implementatie moet worden gebruikt, aangezien we een exemplaar van de generieke WorkflowInterface
-interface in de constructor injecteren? Symfony gebruikt een conventie gebaseerd op de argumentnaam: $commentStateMachine
verwijst naar de comment
workflow in de configuratie (die van het type state_machine
is). Wanneer je een andere argumentnaam probeert zal dit mislukken.
Als je de conventie niet meer weet, gebruik dan het commando debug: container
. Zoek naar alle services met "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
>
Let op keuze 8
, Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
die je vertelt dat het gebruik van $commentStateMachine
als argumentnaam een speciale betekenis heeft.
Note
We hadden het commando debug:autowiring
kunnen gebruiken, zoals we in een vorig hoofdstuk hebben gezien:
1
$ symfony console debug:autowiring workflow
Verder gaan
- Workflows en State Machines en wanneer welke te kiezen;
- De Symfony Workflow documentatie.