Mit einem Workflow Entscheidungen treffen
Einen State (Zustand) für ein Modell zu haben, ist durchaus üblich. Der Kommentar-Zustand wird nur vom Spam-Checker bestimmt. Was passiert, wenn wir weitere Entscheidungsfaktoren hinzufügen?
Vielleicht möchten wir alle Kommentare nach dem Spam-Checker durch Website-Administrator*innen moderieren lassen. Der Prozess würde etwa so aussehen:
- Beginne mit einem
submitted
-Zustand, wenn ein Kommentar von einemr Benutzerin abgegeben wird; - Lasse den Kommentar vom Spam-Checker analysieren und setze den Zustand entweder auf
potential_spam
, aufham
, oder aufrejected
; - Wenn der Zustand nicht
rejected
ist, warte bis eine Website-Administratorin entscheidet, ob der Kommentar gut genug ist und den Zustand aufpublished
oderrejected
ändert.
Die Implementierung dieser Logik ist nicht allzu schwierig, aber Du kannst Dir bestimmt vorstellen, dass das Hinzufügen weiterer Regeln die Komplexität deutlich steigern würde. Anstatt diese Logik selbst zu programmieren, können wir die Symfony Workflow Komponente verwenden:
1
$ symfony composer req workflow
Workflows definieren
Der Kommentar-Workflow kann in der Datei config/packages/workflow.yaml
definiert werden:
Erzeuge eine Visualisierung, um den Workflow zu überprüfen:
1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
Note
Der dot
-Befehl ist Teil des Graphviz -Dienstprogramms.
Einen Workflow verwenden
Ersetze die aktuelle Logik im Message-Handler durch den 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
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -6,7 +6,10 @@ use App\Message\CommentMessage;
use App\Repository\CommentRepository;
use App\SpamChecker;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Workflow\WorkflowInterface;
#[AsMessageHandler]
class CommentMessageHandler
@@ -15,6 +18,9 @@ class CommentMessageHandler
private EntityManagerInterface $entityManager,
private SpamChecker $spamChecker,
private CommentRepository $commentRepository,
+ private MessageBusInterface $bus,
+ private WorkflowInterface $commentStateMachine,
+ private ?LoggerInterface $logger = null,
) {
}
@@ -25,12 +31,18 @@ class CommentMessageHandler
return;
}
- if (2 === $this->spamChecker->getSpamScore($comment, $message->getContext())) {
- $comment->setState('spam');
- } else {
- $comment->setState('published');
+ if ($this->commentStateMachine->can($comment, 'accept')) {
+ $score = $this->spamChecker->getSpamScore($comment, $message->getContext());
+ $transition = match ($score) {
+ 2 => 'reject_spam',
+ 1 => 'might_be_spam',
+ default => 'accept',
+ };
+ $this->commentStateMachine->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()]);
}
-
- $this->entityManager->flush();
}
}
Die neue Logik lautet wie folgt:
- Überprüfe die Nachricht auf Spam, wenn der
accept
-Übergang für den Kommentar in der Nachricht verfügbar ist. - Abhängig vom Ergebnis wendest Du den entsprechenden Übergang an;
- Führe
apply()
aus, um den Kommentar durch einen Aufruf dersetState()
-Methode zu aktualisieren; - Rufe
flush()
auf, um die Änderungen in der Datenbank zu speichern; - Versende die Nachricht erneut, damit der nächste Übergang im Workflow stattfinden kann.
Da wir die Admin-Validierung nicht implementiert haben, wird beim nächsten Verarbeiten der Nachricht "Dropping comment message" geloggt.
Implementieren wir bis zum nächsten Kapitel eine automatische Validierung!
1 2 3 4 5 6 7 8 9 10 11 12
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -41,6 +41,9 @@ class CommentMessageHandler
$this->commentStateMachine->apply($comment, $transition);
$this->entityManager->flush();
$this->bus->dispatch($message);
+ } elseif ($this->commentStateMachine->can($comment, 'publish') || $this->commentStateMachine->can($comment, 'publish_ham')) {
+ $this->commentStateMachine->apply($comment, $this->commentStateMachine->can($comment, 'publish') ? 'publish' : 'publish_ham');
+ $this->entityManager->flush();
} elseif ($this->logger) {
$this->logger->debug('Dropping comment message', ['comment' => $comment->getId(), 'state' => $comment->getState()]);
}
Führe symfony server:log
aus und füge einen Kommentar im Frontend hinzu, um alle Übergänge nacheinander zu sehen.
Services (Dienste) vom Dependency Injection Container finden
Wenn wir Dependency Injection nutzen, bekommen wir Services (Dienste) vom Dependency Injection Container wenn wir als Type-Hint ein Interface oder einen konkreten Klasse-Namen angeben. Aber wenn das Interface mehrere Ausführungen hat, kann Symfony nicht mehr erraten, welches Du meinst. Wir müssen einen Weg finden, um das genau anzugeben.
Gerade solch eine direkte Angabe für die Dependency Injection hatten wir im vorherigen Abschnitt mit der Injection eines WorkflowInterface
.
Wenn wir irgendeine Instanz des generischen `WorkflowInterface-Interface im Contructor angeben, wie kann Symfony dann raten welche Workflow-Anwendung genutzt werden soll? Symfony nutzt eine Konvention basierend auf dem Argument-Namen:
$commentStateMachine bezieht sich auf den
comment-Workflow in der Konfiguration (dessen Typ
state_machine`` ist). Probiere irgendein anderes Argument und es wird fehlschlagen.
Falls Du die Konventionen nicht mehr weisst, nutze den debug:container
-Befehl. Suche nach allen Services (Diensten) die "workflow" beinhalten:
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
>
Siehst Du die Option 8
? Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
sagt Dir, dass die Nutzung von $commentStateMachine
als Argument eine besondere Bedeutung hat.
Note
Wir hätten auch den debug:autowiring
-Befehl nutzen können, wie wir im vorherigen Kapitel gesehen haben:
1
$ symfony console debug:autowiring workflow
Weiterführendes
- Workflows und Zustandsmaschinen und wann man was wählen sollte;
- Die Symfony Workflow Dokumentation.