ワークフローを使って判定する
モデルが状態を持つことはよくあります。今はコメントの状態はスパムチェッカーからしか変わりませんが、他の状態の追加を検討してみましょう。
スパムチェックの後に、Webサイトの管理者が全てのコメントをモデレートしたいとしましょう。このプロセスは次の行のようなものです:
- ユーザーがコメントを追加した際の
submitted
状態から始めましょう; - スパムチェッカーにコメントを分析させ、
potential_spam
,ham
(スパムでないメール),rejected
のいずれかの状態にスイッチさせるようにしましょう; - リジェクトされなければ、Webサイトの管理者がコメントを
published
もしくはrejected
の状態に変更するのを待ちましょう。
ロジックを実装するのはそれほど複雑ではありませんが、さらにルールを追加することで複雑になることもあります。ロジックを自分でコーディングするのではなく、Symfony ワークフローコンポーネントを使用してみます:
1
$ symfony composer req workflow
ワークフローを記述する
コメントワークフローは、config/packages/workflow.yaml
ファイルに記述されます:
ワークフローをバリデートするために、視覚的な表現を生成します:
1
$ symfony console workflow:dump comment | dot -Tpng -o workflow.png
Note
dot
コマンドは、 Graphviz ユーティリティの一部です。
ワークフローを使用する
現在のメッセージハンドラーのロジックをワークフローに置き換えます:
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()]);
+ }
}
}
新しいロジックは以下のようになります:
- メッセージ内のコメントにおいて、
accept
遷移が可能であれば、スパムチェックを行います; - 結果に応じた正しい状態遷移を選んでください;
setState()
メソッドを介して、コメントを更新するためのapply()
を呼んでください;flush()
を呼び、データベースに変更をコミットしてください;- ワークフローの再遷移を許容させるため、メッセージを再ディスパッチしてください。
管理者のバリデーションはまだ実装していないので、メッセージの取得実行をすると "コメントメッセージを削除します" とログが吐かれます。
次の章までに自動バリデーションを実装しましょう:
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()]);
}
symfony server:log
を実行し、フロントエンドでコメントが追加し、順々に状態が遷移することを確認してください。
DIコンテナからサービスを検索する
DIを使う際、インターフェースや場合によっては具体的な実装クラスの名前をタイプヒンティングとしてDIコンテナからサービスを取得しますが、インターフェースが複数の実装クラスを持っている場合、Symfonyはどのクラスが必要か推測することができません。その場合は明示する方法が必要です。
このような例は、前章で WorkflowInterface
を注入した時に出くわしたばかりです。
一般的な WorkflowInterface
インターフェースをコンストラクタで注入すると、Symfonyはどのようにしてどちらのワークフローの実装を使うかを推測するでしょうか? Symfonyは引数名を基にした規約を利用します: $commentStateMachine
は comment
ワークフロー ( state_machine
型) の設定を参照します。
もし規約が思い出せない場合は、 debug:container
コマンドを使いましょう。 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
>
8
, Symfony\Component\Workflow\WorkflowInterface $commentStateMachine
は、 $commentStateMachine
を引数名として使うことに特別な意味があることを伝えています。
Note
前章で見たように debug:autowiring
を使うことができます:
1
$ symfony console debug:autowiring workflow