😅

FormRequestでのユニーク制約に潜む落とし穴!Laravelで学ぶ責務分離を考慮したDBアクセス

2024/12/05に公開

はじめに

Laravel の FormRequest は、データのバリデーションを簡潔に実装できる便利な機能です。しかし、ユニーク制約のように データベース(DB) アクセスが必要なバリデーションでは、適切な層で処理を行わないと、システム設計に悪影響を与える可能性があります。

本記事では、Laravel におけるユニーク制約を例に挙げ、設計思想を考慮した適切なデータ操作のアプローチについて解説します。

具体例

例えば、usersテーブルのemailにユニーク制約を設定する場合、以下のようにリクエストで簡単にバリデーションを追加できます。

CreateRequest.php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Users;

use Illuminate\Foundation\Http\FormRequest;

class CreateRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'email' => [
                'required',
                'string',
                'email:rfc,dns,filter',
                'max:255',
                'unique:users,email', // usersテーブルのemailにユニーク制約を適用
            ],
            // その他略
        ];
    }
}

公式ドキュメントや他のテックブログ等でもこの方法が紹介されており、操作自体は問題ありません。しかし、プロジェクトの設計思想を考慮すると、必ずしもこの方法が適切であるとは限りません。
https://readouble.com/laravel/11.x/ja/validation.html

設計思想

https://zenn.dev/ayumukob/articles/ff183004d09ede

オニオンアーキテクチャと責務分離

今回は、設計思想の一例としてオニオンアーキテクチャを採用した場合を考えます。オニオンアーキテクチャでは、システムを下図ような層に分け、それぞれに責務を分離します。
図は設計思想としてオニオンアーキテクチャを用いた場合の Request の流れを示しています。ユーザから Request が投げられたとき、FormRequest での Validation を行うのは最外層である Presentation 層です。
オニオンアーキテクチャにおけるバリデーションの位置
オニオンアーキテクチャにおける各層の責務とバリデーションの流れ

FormRequest におけるバリデーションの問題点

ユニーク制約の判定にはデータベースアクセスが必要です。そのため、FormRequest でユニーク制約をバリデーションすると、以下の問題が発生します。

  • 責務の逸脱
    • 最外層の Presentation 層でデータベースにアクセスすることは設計思想に反します。
  • テストの独立性の欠如
    • ユニットテストにデータベースのセットアップが必要となり、テストの速度が低下し、独立性が損なわれます。

解決策: ユニーク制約を UseCase 層で処理する

ユニーク制約の判定を UseCase 層で行い、Presentation 層と責務を明確に分離します。Domain 層(RepositoryInterface)を介してユニークチェックを実行し、エラーメッセージを OutputData で返すことで設計思想に適合させます。

CreateUseCase.php
<?php

declare(strict_types=1);

namespace App\Services\UseCases\Users;

// useは省略

/**
 * Class CreateUseCase
 * @package App\Services\UseCases\Users
 */
class CreateUseCase
{
    /**
     * @param UserRepositoryInterface $userRepository ユーザリポジトリ
     */
    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
    ) {
    }

    /**
     * @param InputData $inputData 入力データ
     * @return OutputData
     */
    public function __invoke(InputData $inputData): OutputData
    {
        /** @var CreateInputData $inputData */
        $entity = UserFactory::create($inputData->toArray());

        try {
            DB::beginTransaction();
            $unique = $this->userRepository->unique($entity); //unique判定をする関数の呼び出し
            if (!$unique) {
                // 複数回同じ記述がある場合はUnprocessableEntityOutputDataの中でメソッドを定義
                return UnprocessableEntityOutputData::make()->setValidationMessage([
                    'email' => ['既に登録されているメールアドレスです']
                ]);
            }
        // 以下省略
        }
    }
}

その他のデータ操作の注意点

ユニーク制約以外のデータ操作についても、設計思想に基づいて適切な層で処理することが重要です。
例えば「Test における データ操作は Features テスト と Infrastructure 層以外は禁止」というものです。データ操作とは RefreshDatabaseLite などや Eloquent の create()などが含まれます。これらを正しく理解して使い分けることが重要です。

  • create(): データベースに保存される
  • make(): モデルインスタンスの生成(データベースに保存されない)

最後に

Laravel の便利な機能をそのまま使うだけではなく、「設計に適した方法を選択する」ことが重要です。Request でのユニーク制約のように公式ドキュメントで推奨される機能であっても、プロジェクトの設計方針に合わない場合は、採用を見送ることが求められます。

フレームワークの機能を無制限に使うのではなく、必要に応じてその自由度を設計思想に基づいて制限する判断力も磨いていきたいと思います。

ソーシャルデータバンク テックブログ

Discussion