SymfonyのWorkflow Componentを使ってみた！


\ [Symfonyアドベントカレンダー](https://qiita.com/advent-calendar/2023/symfony)11日目です /

気になってたWorkflow Componentを使ってみたので、そのメモです。

今回、[The Fast Track - 基礎から最速で学ぶ Symfony 入門](https://symfony.com/doc/6.2/the-fast-track/ja/index.html)の[ワークフローを使って判定する](https://symfony.com/doc/6.2/the-fast-track/ja/19-workflow.html)を参考に進めました。

作るものは、カンファレンスに対してコメントを投稿されると、spamチェックをしステータスを管理する機能です。

## 概要
 - [準備](#準備)
   - [ConferenceエンティティとCommentエンティティを作成し、関連づける](#conferenceエンティティとcommentエンティティを作成し関連づける)
   - [ConferenceControllerを作成する](#conferencecontrollerを作成する)
   - [非同期で簡易的なspamチェックをするようにする](#非同期で簡易的なspamチェックをするようにする)
 - [ワークフローを使ってステータスを管理するようにする](#ワークフローを使ってステータスを管理するようにする) 👈ココがメイン
   - [symfony/workflowをインストールする](#symfonyworkflowをインストールする)
   - [ワークフローを作成する(yaml)](#ワークフローを作成するyaml)
   - [ワークフロー図を生成する](#ワークフロー図を生成する)
   - [ワークフローを設定する](#ワークフローを設定する)
 - [番外編: Symfony ProfilerにWorkflowが追加された :tada: ※Symfony6.4以上](#番外編-symfony-profilerにworkflowが追加されました-tada-symfony64以上)

## 準備
ここがメインではないので使ったコマンドとDB概要のみ記載します。

### ConferenceエンティティとCommentエンティティを作成し、関連づける

```shell
$ composer require symfony/maker-bundle --dev
$ composer require orm
$ symfony console make:entity Comment
$ symfony console make:entity Conference
```

```shell
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を作成する

```shell
$ symfony console make:controller ConferenceController
$ composer req symfony/asset twig/intl-extra:^3
$ composer require form validator
```

### 非同期で簡易的なspamチェックをするようにする

```shell
$ symfony composer req doctrine-messenger
$ symfony console messenger:consume async -vv
```

参考：
 - [コントローラーを作成する](https://symfony.com/doc/6.2/the-fast-track/ja/6-controller.html)
 - [データベースをセットアップする](https://symfony.com/doc/6.2/the-fast-track/ja/7-database.html)
 - [データ構造の説明](https://symfony.com/doc/6.2/the-fast-track/ja/8-doctrine.html)
 - [ユーザーインターフェースを構築する](https://symfony.com/doc/6.2/the-fast-track/ja/10-twig.html)
 - [非同期にする](https://symfony.com/doc/6.2/the-fast-track/ja/18-async.html)

## ワークフローを使ってステータスを管理するようにする

### symfony/workflowをインストールする

```shell
$ composer require symfony/workflow
```

### ワークフローを作成する(yaml)

config/packages/workflow.yaml
```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

```
### ワークフロー図を生成する

```shell
symfony console workflow:dump comment | dot -Tpng -o workflow.png
```
こんな図を作成できます :sunglasses: 
![workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83896/d94ce0b6-5208-66c2-cbbd-c58d2644021c.png)

### ワークフローを設定する
ConferenceController.php
```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
<?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`に遷移しました。

```shell
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 |
+----+---------------+--------+---------+------------------+---------------------+-----------+

```

```shell
$ 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が追加されました :tada: ※Symfony6.4以上

![スクリーンショット 2023-12-11 16.46.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83896/f01088c2-1da4-4e14-aaa4-5fe9d9681355.png)


## さいごに
yamlにルールを記載できたり、ワークフロー図を生成できたり超便利だなと思いました。

今回、初めてだったので[The Fast Track](https://symfony.com/doc/6.2/the-fast-track/ja/index.html)を参考にコメントのステータス管理で試してみました。
メジャーな機能だとTODOリストのステータス管理などに使えそうだなと思いました！