Prendre des décisions avec un workflow
Avoir un état pour un modèle est assez commun. L'état du commentaire n'est déterminé que par le vérificateur de spam. Et si on ajoutait d'autres critères de décision ?
Nous pourrions laisser l'admin du site modérer tous les commentaires après le vérificateur de spam. Le processus serait quelque chose comme :
- Commencez par un état
submitted
lorsqu'un commentaire est soumis par un internaute ; - Laissez le vérificateur de spam analyser le commentaire et changer l'état en
potential_spam
,ham
ourejected
- S'il n'est pas rejeté, attendez que l'admin du site décide si le commentaire est suffisamment utile en changeant l'état pour
published
ourejected
.
La mise en œuvre de cette logique n'est pas trop complexe, mais vous pouvez imaginer que l'ajout de règles supplémentaires augmenterait considérablement la complexité. Au lieu de coder la logique nous-mêmes, nous pouvons utiliser le composant Symfony Workflow :
1
$ symfony composer req workflow
Décrire des workflows
Le workflow de commentaires peut être décrit dans le fichier config/packages/workflow.yaml
:
Pour valider le workflow, générez une représentation visuelle :
1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
Note
La commande dot
fait partie de l'utilitaire Graphviz.
Utiliser un workflow
Remplacez la logique actuelle dans le gestionnaire de messages par le 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();
}
}
La nouvelle logique se lit comme ceci :
- Si la transition
accept
est disponible pour le commentaire dans le message, vérifiez si c'est un spam ; - Selon le résultat, choisissez la bonne transition à appliquer ;
- Appellez
apply()
pour mettre à jour le Comment via un appel à la méthodesetState()
; - Appelez
flush()
pour valider les changements dans la base de données ; - Réexpédiez le message pour permettre au workflow d'effectuer une nouvelle transition.
Comme nous n'avons pas implémenté la fonctionnalité de validation par l'admin, la prochaine fois que le message sera consommé, le message "Dropping comment message" sera enregistré.
Mettons en place une validation automatique en attendant le prochain chapitre :
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()]);
}
Exécutez symfony server:log
et ajoutez un commentaire sur le site pour voir toutes les transitions se produire les unes après les autres.
Trouver des services depuis le conteneur d'injection de dépendances
Quand nous utilisons l'injection de dépendances, nous récupérons des services depuis le conteneur d'injection de dépendances en utilisant le typage par interface ou parfois par une implémentation de classe concrète. Mais quand une interface à plusieurs implémentations, Symfony ne peut deviner celle dont vous avez besoin. Nous avons besoin d'être explicite.
Nous venons juste de rencontrer un cas semblable avec l'injection de WorkflowInterface
dans la section précédente.
Comme nous injectons n'importe quelle instance de l'interface générique WorkflowInterface
dans le constructeur, comment Symfony peut savoir quelle implémentation du workflow utiliser ? Symfony utilise une convention basée sur le nom de l'argument : $commentStateMachine
fait référence au workflow comment
de la configuration (dont le type est state_machine
). Essayez n'importe quel autre argument et l'injection échouera.
Si vous ne vous rappelez pas de la convention, utilisez la commande debug:container
. Cherchez tous les services contenant "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
>
Remarquez le choix 8
, Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
qui vous indique qu'utiliser $commentStateMachine
comme argument nommé a une signification particulière.
Note
Nous aurions pu utiliser la commande debug:autowiring
comme vu dans un précédent chapitre :
1
$ symfony console debug:autowiring workflow