【PHPフレームワークFLow】GETリクエストだけどDB更新したい時の対処

# 初めに
PHPフレームワークFLowでは、GETリクエストの処理の中ではDB更新ができないという仕様になっています。そんな中、どうしてもGETの中でDB更新がしたいという場合もあるかと思います。今回はそんな場合の対処法について解説します。

# GETの場合DB更新ができない
実際に試してみました。
以下で作成したAPIにGETリクエストを送ってみたいと思います。

```php: Getだけど登録を試みたAPI
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Controller\ActionController;

class PersistanceTestController extends ActionController
{

    /**
     * @Flow\Inject
     * @var \Neos\Welcome\Domain\Repository\ItemRepository
     */
    protected $itemRepository;

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @return string
     */
    public function getButWantToRegisterAction()
    {   
        // GETだけどデータ登録
        $this->itemRepository->addItem(
            'id', 'name', 100, 'storeId', 'storeName', 'address'
        );
        $this->view->assign('value', [
            "Success Registeration"
        ]);
    }
}
```

実際に送ってみた結果がこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/35e22e7b-c5ff-153c-b779-77d7ada180ac.png)


一部エラー文を抜粋しました↓
> According to the HTTP 1.1 specification, so called "safe request" (usually GET or HEAD requests)
should not change your data on the server side and should be considered read-only. If you need to add,
modify or remove data, you should use the respective request methods (POST, PUT, DELETE and PATCH).

GETとHEADは**safe request**として扱われており、登録は許してないとのこと。困った困った。

# なぜできないのか
ということでどこのコードで制御されているのか探してきました。
`Package.php`に以下のようなコードがあります。

```php: Package.phpでデータ更新の有無を確認している
class Package extends BasePackage
{
    public function boot(Core\Bootstrap $bootstrap)
    {
        // 中略
        
        $dispatcher->connect(Mvc\Dispatcher::class, 'afterControllerInvocation', function ($request) use ($bootstrap) {
            // No auto-persistence if there is no PersistenceManager registered
            if (
                $bootstrap->getObjectManager()->has(Persistence\PersistenceManagerInterface::class)
            ) {
                if (!$request instanceof Mvc\ActionRequest || SecurityHelper::hasSafeMethod($request->getHttpRequest()) !== true) {
                    $bootstrap->getObjectManager()->get(Persistence\PersistenceManagerInterface::class)->persistAll();
                } elseif (SecurityHelper::hasSafeMethod($request->getHttpRequest())) {
                    $bootstrap->getObjectManager()->get(Persistence\PersistenceManagerInterface::class)->persistAllowedObjects();
                }
            }
        });
        
        // 中略
    }
}
```
このコードを見るに、以下のような処理になってそうです。
  * `Dispatcher`クラスの`afterControllerInvocation`というメソッドが呼び出される際に、クロージャで定義している処理が走る
  * クロージャーの処理は、
    * **safe request**ではない場合は`persistAll()`でDB更新
    * そうでない場合は`persistAllowedObjects()`で**許可されているオブジェクトのみ**DB更新


DispacherはFlowにおけるControllerを呼び出しているクラスです。`afterControllerInvocation`は名前的にControllerの後に呼び出される処理のようですね。

また、`persistAll()`はどんなオブジェクトでもDB更新を許可するのに対し、`persistAllowedObjects()`はあらかじめ許可をしておいたオブジェクトのみ更新するという違いがありそうです。

# 対処法
以上から、以下のような対処法が浮かびました。
* 案1：Dispacherに処理が返る前に、ActionController内でpersistAllする
* 案2：更新対象のオブジェクトを`AllowedObjects`に追加する
* 案3：safe requestの一覧からGETをなくす

## 案1：Dispacherに処理が返る前に、ActionController内でpersistAllする

一番現実的な案です。
APIごとに処理を変えることができるので便利ですしね。

```php: 
    public function getButWantToRegisterAction()
    {   
        $this->itemRepository->addItem('id', 'name', 100, 'storeId', 'storeName', 'address');
        // ActionControllerの処理を終える前にpersistAll()を実行
        $this->persistenceManager->persistAll();
        $this->view->assign('value', [
            "Success Registeration"
        ]);
    }
```

## 案2：更新対象のオブジェクトを`AllowedObjects`に追加する
オブジェクトごとにDB更新していいかどうかを設定したいならこの案かなと思います。

これについては実際まだ試せていないので、できるのかどうかも含めて今度検証してみます。

（今までFlowを触ってきた感じでは、`settings.yaml`などに記述するのかなと思っていますが、真相は分からず）


## 案3：safe requestの一覧からGETをなくす
個人的に一番微妙な案です。
`safe request`の一覧はFW内の`SecurityHelper`というクラスに直で書かれています。このリストから削除するという案です。

```php: safe requestの一覧はSecurityHelperクラスに書かれている
<?php
namespace Neos\Flow\Http\Helper;

use Psr\Http\Message\RequestInterface;

/**
 * Helper functions about request safety and security.
 */
abstract class SecurityHelper
{
    public static function isSecureRequest(RequestInterface $request): bool
    {
        // TODO: Isn't there a better way to figure this out?
        return $request->getUri()->getScheme() === 'https';
    }

    public static function hasSafeMethod(RequestInterface $request): bool
    {
        return (in_array($request->getMethod(), ['GET', 'HEAD']));
    }
}

```
FWのソースを直接修正するのは微妙なので、やるならこのクラスを継承した独自クラスを作成してやる感じになるかと思います。

## 終わりに
ということで、GETリクエストだけどDB更新したい時の対処を書きました。
正直使いどころがあるか微妙ですが、困ってる方は試してみてください。

ここまでご覧いただきありがとうございました！