\ Symfonyアドベントカレンダー11日目です /
気になってたWorkflow Componentを使ってみたので、そのメモです。
今回、The Fast Track - 基礎から最速で学ぶ Symfony 入門のワークフローを使って判定するを参考に進めました。
作るものは、カンファレンスに対してコメントを投稿されると、spamチェックをしステータスを管理する機能です。
概要
準備
ここがメインではないので使ったコマンドとDB概要のみ記載します。
ConferenceエンティティとCommentエンティティを作成し、関連づける
$ composer require symfony/maker-bundle --dev
$ composer require orm
$ symfony console make:entity Comment
$ symfony console make:entity Conference
mysql> desc conference;
+------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| city | varchar(255) | NO | | NULL | |
| year | varchar(4) | NO | | NULL | |
| is_international | tinyint(1) | NO | | NULL | |
+------------------+--------------+------+-----+---------+----------------+
4 rows in set (0.02 sec)
mysql> desc comment;
+---------------+--------------+------+-----+-----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+-----------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| conference_id | int(11) | NO | MUL | NULL | |
| author | varchar(255) | NO | | NULL | |
| text | longtext | NO | | NULL | |
| email | varchar(255) | NO | | NULL | |
| created_at | datetime | NO | | NULL | |
| state | varchar(255) | NO | | submitted | |
+---------------+--------------+------+-----+-----------+----------------+
7 rows in set (0.00 sec)
ConferenceControllerを作成する
$ symfony console make:controller ConferenceController
$ composer req symfony/asset twig/intl-extra:^3
$ composer require form validator
非同期で簡易的なspamチェックをするようにする
$ symfony composer req doctrine-messenger
$ symfony console messenger:consume async -vv
参考:
ワークフローを使ってステータスを管理するようにする
symfony/workflowをインストールする
$ composer require symfony/workflow
ワークフローを作成する(yaml)
config/packages/workflow.yaml
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
- spam
- rejected
- published
transitions:
accept:
from: submitted
to: ham
reject_spam:
from: submitted
to: spam
publish_ham:
from: ham
to: published
reject_ham:
from: ham
to: rejected
ワークフロー図を生成する
symfony console workflow:dump comment | dot -Tpng -o workflow.png
ワークフローを設定する
ConferenceController.php
class ConferenceController extends AbstractController
{
#[Route('/conference/{id}', name: 'conference')]
public function show(Request $request, Environment $twig, Conference $conference, CommentRepository $commentRepository): Response
{
$comment = new Comment();
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$comment->setConference($conference);
$this->entityManager->persist($comment);
$this->entityManager->flush();
$this->bus->dispatch(new CommentMessage($comment->getId()));
...
CommentMessageHandler.php
<?php
declare(strict_types=1);
namespace App\MessageHandler;
use App\Message\CommentMessage;
use App\Repository\CommentRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Workflow\WorkflowInterface;
#[AsMessageHandler]
class CommentMessageHandler
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly CommentRepository $commentRepository,
private readonly MessageBusInterface $bus,
private readonly WorkflowInterface $commentStateMachine,
)
{
}
public function __invoke(CommentMessage $message): void
{
$comment = $this->commentRepository->find($message->getId());
if (!$comment) {
return;
}
if ($this->commentStateMachine->can($comment, 'accept')) {
$transition = 'accept';
// 超簡易的なspamチェック
if (str_contains($comment->getText(), 'test')) {
$transition = 'reject_spam';
}
$this->commentStateMachine->apply($comment, $transition);
$this->entityManager->flush();
$this->bus->dispatch($message);
} elseif ($this->commentStateMachine->can($comment, 'publish_ham')) {
$this->commentStateMachine->apply($comment, 'publish_ham');
$this->entityManager->flush();
}
}
}
こんな感じで、コメント内容に「test」が含まれている場合はstatusがspam
に遷移し、
それ以外の場合はham
->published
に遷移しました。
mysql> select * from comment;
+----+---------------+--------+---------+------------------+---------------------+-----------+
| id | conference_id | author | text | email | created_at | state |
+----+---------------+--------+---------+------------------+---------------------+-----------+
...
| 9 | 2 | taro | test | taro@example.com | 2018-01-01 00:00:00 | spam |
| 10 | 2 | jiro | hey | jiro@example.com | 2018-01-01 00:00:00 | published |
+----+---------------+--------+---------+------------------+---------------------+-----------+
$ symfony console messenger:consume async -vv
[OK] Consuming messages from transport "async".
// The worker will automatically exit once it has received a stop signal via the messenger:stop-workers command.
// Quit the worker with CONTROL-C.
[info] Received message App\Message\CommentMessage
[info] Leaving "submitted" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Transition "reject_spam" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Entering "spam" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Sending message App\Message\CommentMessage with async sender using Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport
[info] Message App\Message\CommentMessage handled by App\MessageHandler\CommentMessageHandler::__invoke
[info] App\Message\CommentMessage was handled successfully (acknowledging to transport).
[info] Received message App\Message\CommentMessage
[info] Leaving "submitted" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Transition "accept" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Entering "ham" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Sending message App\Message\CommentMessage with async sender using Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport
[info] Message App\Message\CommentMessage handled by App\MessageHandler\CommentMessageHandler::__invoke
[info] App\Message\CommentMessage was handled successfully (acknowledging to transport).
[info] Received message App\Message\CommentMessage
[info] Leaving "ham" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Transition "publish_ham" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Entering "published" for subject of class "App\Entity\Comment" in workflow "comment".
[info] Message App\Message\CommentMessage handled by App\MessageHandler\CommentMessageHandler::__invoke
[info] App\Message\CommentMessage was handled successfully (acknowledging to transport).
番外編: Symfony ProfilerにWorkflowが追加されました ※Symfony6.4以上
さいごに
yamlにルールを記載できたり、ワークフロー図を生成できたり超便利だなと思いました。
今回、初めてだったのでThe Fast Trackを参考にコメントのステータス管理で試してみました。
メジャーな機能だとTODOリストのステータス管理などに使えそうだなと思いました!