以前に書いた Laravel のバリデーションルール exists に Eloquent を使うの強化版です
背景
以前書いた記事ではカラムやその値の無意識化を目的に、コンストラクタでモデル名を受け取り、存在をクエリするカスタムバリデーションルールを定義しました。
$this->modelName::where($this->propertyName, $value)->exists()
しかし、この解決策には次の問題がありました
- 渡された値と DB 上の値の型が一致しないと SQL エラーになる場合がある
- 整数の場合に負数を渡してもクエリが走る
ルールをいくつか追加すると解決できますが、複数のエラーは出せないなどアプリの仕様に影響する、カラムやその値の無意識化を目的としているので避けたいところです。
'id' => [
'required',
'integer',
'min:1', // 主キーのカラムは 1 以上
'max:2147483647', // 主キーのカラムは DB の integer の範囲
'bail', // どこかで失敗したら Exists はバリデートしない
new Exists(Hoge::class),
],
標準の exists では例外的に他のルールで失敗したらバリデートはしない対応をしているようです。
カスタムバリデーションルールでは他のルールの失敗を得られないため、別の対応が必要そうです。
新しい解決策
悩んだうえ、できた新しいルールのソースコードが次
ほとんど主キーで探すことが多いので、対象を integer の主キーに絞ることで解決を図りました。
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class ExistsKey implements Rule
{
protected $modelName;
const INTEGER_MAX = 2147483647;
public function __construct(string $modelName)
{
$this->modelName = $modelName;
}
public function passes($attribute, $value)
{
// 整数でないときはバリデートしない。エラーは別のルールで出す。
if (validator(['value' => $value], ['value' => 'integer'])->fails()) {
return true;
}
if ($value < 1 || $value > self::INTEGER_MAX) {
return false;
}
return $this->modelName::whereKey($value)->exists();
}
public function message()
{
return ':attribute はデータが存在しません';
}
}
使い方
リクエストで渡された値がモデルの主キーとして存在するか調べます。
use App\Hoge;
use App\Rules\ExistsKey;
'hoge_id' => [
'required',
'integer',
new ExistsKey(Hoge::class), // これ
],
追伸
使ってみるとそこそこ使えますが、対象を主キーに絞ったので、主キー以外のカラムで調べるときは別にクエリして調べる必要があります。
また内部的には整数であるかのバリデーションを 2 回することになります。
もっといい方法はないものか。