【PHPフレームワークFLow】Flowで楽観ロックを実装する

# 初めに
PHPフレームワークFlowで楽観ロックを実装しようとした際、FW側で何かメソッドなど用意しているのかどうか気になりました。今回はその結果をアウトプットします。

# Doctrineの楽観ロック実装方法
PHPフレームワークFlowはDoctrineというORMを標準で利用しています。
Doctrineには楽観ロックを行うためのメソッドが最初から用意されています。

https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/transactions-and-concurrency.html#optimistic-locking

一部サンプルソースを抜き出してきました。
それぞれ、モデルクラスと楽観ロックを利用するクラスです。
```php
class User
{
    // ...
    #[Version, Column(type: 'datetime')]
    private DateTime $version;
    // ...
}
```

```php
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;
$expectedVersion = 184;

try {
    $entity = $entityManager->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);

    // do the work

    $entityManager->flush();
} catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}
```

ポイントは以下です。
* **モデルクラスのバージョンを持つカラムに`#[Version]`アトリビュートを付与する**
* **EntityManagerでデータを取得する際、`LockMode::OPTIMISTIC`と期待するバージョン情報を指定する**
* **flushで永続化する際、バージョンがすでに更新されていた場合は`OptimisticLockException`が投げられる**


# Flowには楽観ロックのためのメソッドがなさそう
Flowには`PersistenceManager`という`EntityManager`をラップしているクラスが存在します。
しかし、`PersistenceManager`には`EntityManager`のような楽観ロックに対応したメソッドは用意されていませんでした。

そのため楽観ロックを行うのであれば
* 自力で実装する
* Doctrineの`EntityManager`を用いる

のどちらかで実装することになります。
今回は後者を試してみました。

# 実際に試してみる
ということで実装してみましょう。

## Modelの作成
まずはModelクラスを作成します。
アトリビュートではなくアノテーションを使用していますが、意味は同じです。

```php: UserDetail
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="user_detail")
 */
class UserDetail
{
    /**
     * @ORM\Column(name="name")
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(name="email")
     * @var string
     */
    protected $email;

    /**
     * @ORM\Column(name="password")
     * @var string
     */
    protected $password;

    /**
     * @ORM\Version
     * @var int
     */
    protected $version;

    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getVersion(): int
    {
        return $this->version;
    }
}
```

## EntityManagerの使用
続いて、`EntityManager`を用いて楽観ロックを行う処理を作成します。
今回はController内に実装しました。


```php: OptimisticLockingController
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

class OptimisticLockingController extends \Neos\Flow\Mvc\Controller\ActionController
{
    
    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @Flow\Inject
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * 楽観ロックを試すクラス
     */
    public function optimisticLockingAction(string $theEntityId)
    {
        try {
            // 現在のユーザ情報取得
            $oldUserDetail = $this->entityManager->find('\Neos\Welcome\Domain\Model\UserDetail', $theEntityId);

            // データを書き換える時間稼ぎ
            sleep(10);

            // 更新用のユーザ情報を楽観ロックで取得
            $newUserDetail = $this->entityManager->find('\Neos\Welcome\Domain\Model\UserDetail', $theEntityId, LockMode::OPTIMISTIC, $oldUserDetail->getVersion());
            $newUserDetail->setName('Updated Name');

            // データ更新
            $this->entityManager->persist($newUserDetail);
            $this->entityManager->flush();
            $this->view->assign('value', 'User updated');
        } catch(OptimisticLockException $e) {
            $this->view->assign('value', 'OptimisticLockException');
        }
    }
}
```

## 実行してみる
ということで実行してみましょう。
今回の方針は。
**1. APIを実行する**
**2. sleepさせ、手でデータを更新する**
**3. sleepが終わり、楽観ロックエラーが発生する**

という手順で試していきます。

初期データは以下です。

```sh
mysql> select * from user_detail\G
*************************** 1. row ***************************
persistence_object_identifier: 0ac51f7d-c002-44fd-8cca-e60ac3cfee8b
                         name: default name
                        email: test@example.jp
                     password: password
                      version: 1
1 row in set (0.00 sec)
```

### 1. APIを実行する

```sh
>curl -i -X PUT ^
More?    -H "Content-Type: application/json" ^
More?    -d "{\"theEntityId\": \"0ac51f7d-c002-44fd-8cca-e60ac3cfee8b\"}" ^
More?    "http://localhost:8081/Neos.Welcome/OptimisticLocking/optimisticLocking"
```

### 2. sleepさせ、手でデータを更新する
sleepしている間にデータを更新します。


```sh
mysql> update user_detail set name='warikomi name', version = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> select * from user_detail\G
*************************** 1. row ***************************
persistence_object_identifier: 0ac51f7d-c002-44fd-8cca-e60ac3cfee8b
                         name: warikomi name
                        email: test@example.jp
                     password: password
                      version: 2
1 row in set (0.00 sec)

mysql>
```
無事に更新できましたね。

### 3. sleepが終わり、楽観ロックエラーが発生する

そうこうしてる間に、レスポンスが返ってきました。
エラーを握りつぶしているので200ですが、catchのルートで設定した`OptimisticLockException`がレスポンスで返ってきています。
```sh
HTTP/1.1 200 OK
Host: localhost:8081
Date: Fri, 14 Jun 2024 11:52:04 GMT
Connection: close
X-Powered-By: PHP/8.2.17
Content-Type: application/json
X-Flow-Powered: Flow/8.3
Content-Length: 25

"OptimisticLockException"
```

最後にDBを見て、データ更新されてないことが確認できました。
```sh
mysql> select * from user_detail\G
*************************** 1. row ***************************
persistence_object_identifier: 0ac51f7d-c002-44fd-8cca-e60ac3cfee8b
                         name: warikomi name
                        email: test@example.jp
                     password: password
                      version: 2
1 row in set (0.00 sec)

mysql>
```

# 終わりに
今回はFlowで楽観ロックを実装する方法を解説しました。
調査時にFWの中まで見に行くのはやっぱり面白いし、ちゃんと実装できるとさらに面白いですね。

今回は以上です。
ここまでご覧いただきありがとうございました！