Schritt 19: Mit einem Workflow Entscheidungen treffen

5.2 version
Maintained Unmaintained
5.0

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 einem*r Benutzer*in abgegeben wird;
  • Lasse den Kommentar vom Spam-Checker analysieren und setze den Zustand entweder auf potential_spam, auf ham, oder auf rejected;
  • Wenn der Zustand nicht rejected ist, warte bis ein*e Website-Administrator*in entscheidet, ob der Kommentar gut genug ist und den Zustand auf published oder rejected ä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:

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

Erzeuge eine Visualisierung, um den Workflow zu überprüfen:

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

Bemerkung

Der dot-Befehl ist Teil des Graphviz -Dienstprogramms.

Einen Workflow verwenden

Ersetze die aktuelle Logik im Message-Handler durch den 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');
-        }

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

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 der setState()-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!

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

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.

Bemerkung

Wir hätten auch den debug:autowiring-Befehl nutzen können, wie wir im vorherigen Kapitel gesehen haben:

1
$ symfony console debug:autowiring workflow

  • « Previous Schritt 18: Asynchrone Verarbeitung
  • Next » Schritt 20: E-Mails an Administrator*innen senden

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