【Laravel】論理削除されていないレコードの範囲でユニークであるかのバリデーション

 バリデーションは値が正当かを検証する処理のことです。バリデーションでは時折、二重登録防止などの目的でユニークルール(ある値が既にDB(データベース)内に存在しないならばOK、存在するならばOUTなルール)を用います。削除処理が単純にDBのテーブル中のレコードが物理削除されるデータの場合、ただ単に既にDB中のテーブルのあるレコード中のあるカラムに要求された値が存在するかどうかを確認するのみで済みます。例えば次の様なSQLを発行して1が返ってこれば既に使用済みなのでバリデーションエラー、0が返ってこれば未使用なのでバリデーションをパスといった具合です。

SELECT EXISTS(SELECT * FROM members WHERE email = 'hoge@example.com');

 よくあるテーブルの構造に論理削除があります。これはあるカラムの値を元に、削除されていないレコードと、削除されたかの様に振る舞うレコードを分ける手法です。PHPフレームワークのLaravelではこれを運用しやすくするために次の様に自然と論理削除を適用したSQL文を発行する様になっています。

Member::where('email', 'hoge@example.com')->get();
// ↑のコードで↓のSQL文が発行
"select * from `members` where `email` = hoge@example.com and `members`.`deleted_at` is null"

 この仕組みは便利ですがユニークルールでは無視したくなる時があります。例えば、一度退会したユーザが再度会員として登録する時です。退会時に以前の会員データを論理削除したとし、会員登録時にユニークルールによるバリデーションを行うとします。こうすると会員登録時に以前退会したデータと同じ値を入力することができなくなります。以前と同じ値を入力すると論理削除済みのデータを参照してユニークな値でないとしてバリデーションエラーが発生してしまいます。
 これを回避するために論理削除されていないレコードの範囲でのみのユニークルールを作る必要があります。 これを Laravel のユニークルールクラスを用いて記述すると次の様になります。

        // バリデーションルール定義
        $rules = [
            'email' => [],
            /** 省略 */
        ];
        // DB 定義を直書きしないために Eloquent インスタンスを用意
        $user = new User();
        // Rule::Unique はクエリをメソッドチェーンで引っ付けられ、そのクエリ範囲でユニークルールを適用します
        // テーブル名と論理削除カラムを Eloquent インスタンスから呼び出してユニークルールを定義します
        $uniqEmail = Rule::Unique($user->getTable(), 'email')
            ->whereNull($user->getDeletedAtColumn());
        // ユニークルールを元々のバリデーションルールに追加
        $rules['email'][] = $uniqEmail;

 Laravel のユニークルールは次のドキュメントの通りに動作します。これを利用すると上記コード例の様に論理削除されていないレコードの範囲のみのユニークルールを定義できます。
バリデーション 6.x Laravel # unique

 いちいちクエリを書くのが面倒な人は Rule クラスを定義するのも手です。これは次のコードで実現できます。

<?php

namespace App\Rules;

use App\Models\Eloquents\BaseEloquent;
use Illuminate\Contracts\Validation\Rule;

/**
 * 論理削除されていないレコードの範囲のみでのユニークルール
 * Class UniqueInNotSoftDeleted
 * @package App\Rules
 */
class UniqueInNotSoftDeleted implements Rule
{
    /** @var BaseEloquent unique ルールで参照するテーブルのインスタンス */
    protected $eloquentInstance;
    /** @var string unique ルールを適用するカラム */
    protected $column;
    /** @var string|int|null unique ルールから除外するレコードのID */
    protected $excludeId;

    /**
     * UniqueInNotSoftDeleted constructor.
     * @param BaseEloquent    $eloquentInstance unique ルールで参照するテーブルのインスタンス
     * @param string          $column           unique ルールを適用するカラム
     * @param string|int|null $excludeId        unique ルールから除外するレコードのID
     */
    public function __construct(BaseEloquent $eloquentInstance, string $column, $excludeId = null)
    {
        $this->eloquentInstance = $eloquentInstance;
        $this->column           = $column;
        $this->excludeId        = $excludeId;
    }

    /**
     * @param  string  $attribute
     * @param  mixed   $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // ベースになるクエリ。与えられたカラムに現在参照している値のレコードがあるかないかを確かめるための where
        $query = $this->eloquentInstance->newQuery()
            ->where($this->column, $value);
        // 論理削除を考慮するための if 与えられた Eloquent インスタンスが論理削除を定義するカラムの情報を持っているか否か
        if (method_exists($this->eloquentInstance, 'getDeletedAtColumn')) {
            // 論理削除カラムが分かるならば、そのカラムの値が null である論理削除されていないレコードのみの範囲でクエリを実行するための where
            $query = $query->whereNull($this->eloquentInstance->getDeletedAtColumn());
        }
        // 特定の ID のレコードを除外するよう要求されているか確認
        if (isset($this->excludeId)) {
            // もし↑の要求があるならば、その ID 以外のレコードのみの範囲にするための where
            $query = $query->where($this->eloquentInstance->getKeyName(), '<>', $this->excludeId);
        }

        // ここまでで構築したクエリ範囲内に現在参照している値 $value と同じ値を持つレコードが存在しないならば true
        return ! $query->exists();
    }

    /**
     * バリデーションエラーメッセージ
     * @return array|string
     */
    public function message()
    {
        return ':attributeの値は既に存在しています。';
    }
}

 使用例は次です。

    // バリデーションルール定義
    $rules = [
        'email' => [new UniqueInNotSoftDeleted(new User(), 'email', request()->userId)],
        /** 省略 */
    ];

 呼び出し側のコードがシンプルになりました。再利用するならばこちらの方が断然良いです。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG