ビジネスロジックが複雑なときはRequestオブジェクトを作るといいんじゃないかというお話

この記事は Relux AdventCalendar 6日目の記事です。

Requestレイヤーを導入した話

Webアプリケーションのソースコードは、リクエストデータの妥当性チェックとデータ整合性チェックとデータ整形などなど、往々にして長くなりがちです。ビジネスロジックが複雑になっていくほど、データ整合性のチェックや、外部サービス連携のためのデータ整形の処理が膨らんでいきます。

そこでリクエストデータを精査してアプリケーションに使いやすい形で渡す役割として、Requestオブジェクトというレイヤーを導入してみました。

Requestレイヤーの役割

リクエストデータのバリデーション

フォームからPOSTされた値の「必須項目」や「値型」のバリデーションだけでなく、DBのデータを参照してビジネス要件的に妥当なリクエストかどうかの確認もRequestオブジェクトが行います。

# ReservationRequest.php

class ReservationRequest extends Request
{
    public $validate = [
        'plan_id' => [
            'notEmpty' => [
                'rule' => ['notEmpty'],
            ],
            'exists' => [
                'rule' => ['planExists'],
            ],
            'inSalesPeriod' => [
                'rule' => ['inSalesPeriod'],
            ],
        ],
    ];

    /**
     * 宿泊プランが存在するかチェックする  
     */
    public function planExists()
    {
        return $this->plan() ? true : false;
    }

    /**
     * 宿泊プランが利用可能な日程かチェックする
     */
    public function inSalesPeriod()
    {
        $plan = $this->plan();
        $schedule = $this->schedule();
        return $plan->inSalesPeriod($schedule);
    }

    /**
     * 宿泊プランを返す
     */
    private function plan()
    {
        if (!$this->hasCache('plan')) {
            $model = ClassRegistry::init('Plan');
            $plan = $model->findById($this->get('plan_id'));
            $this->setCache('plan', $plan ? $plan : null);
        }
        return $this->getCache('plan');
    }

    /**
     * 宿泊日程オブジェクトを返す
     */
    private function schedule()
    {
        return new Schedule(
            new DateTimeImmutable($this->get('checkin')),
            new DateTimeImmutable($this->get('checkout'))
        );
    }

この擬似コードでは予約フォームからリクエストされたプランIDとチェックイン日・チェックアウト日が実際に予約できるかまでバリデーションをかけています。

リクエストデータの整形

カスタマーにできるだけストレスを感じずに予約してもらえるようにある程度の入力ミスを許容して、サーバー側で変換することも大切です。

例えば電話番号を全角数字で入力してしまった場合に、半角数字に変換して後続処理に回すということは良くやると思いますが、これもRequestクラスが行います。

コントローラーで処理するオブジェクトを生成して返す

Requestオブジェクトを実装した当初は、整形したリクエストデータを返してくれるオブジェクトとしてコントローラーで使いまわして、予約Entityを生成し、予約処理を実行することを想定していました。

しかし予約はWebブラウザだけでなくAPIなど複数のフローが存在します。そのフローごとに1度にリクエストされるパラメータが異なるため、予約Entityのプロパティがどこまでセットされるかはそのフロー(リクエスト)に依存します。そこに対応するためにEntity生成までをRequestオブジェクトの責務としました。

# ReservationRequest.php

    /**
     * 予約データを生成する
     */
    public function toEntity()
    {
        $model = ClassRegistry::init('Reservation');
        $plan = $this->plan();
        $schedule = $this->schedule();
        $properties = ['Reservation' => [
            "plan_id" => $plan->id,
            "checkin" => $schedule->checkin(),
            "checkout" => $schedule->checkout(),
            // more properties...
        ]];
        return $model->entity($properties);
    }

RequestオブジェクトからEntityを生成してもらったコントローラーは、予約処理フローを流すことだけに集中してもらうことで、可読性を高くすることができました。

まとめ

ビジネスロジックのゴタゴタをコントローラーに持っていく前に、Requestレイヤーに吸収してもらうと幸せになれます。