Laravel 8 カスタムルール用のインターフェース DataAwareRule と ValidatorAwareRule で戯れてみる
はじめに
Laravel 8.42、8.43で追加されたカスタムルール用のインターフェース DataAwareRule と ValidatorAwareRule で戯れてみました。
DataAwareRule は、無くてもどうにかなったり、ValidatorAwareRule の方は、代わりにAfterフックとか使えば何とかなったりもしますが、これらを使うとより綺麗に決めることができます。
なお、執筆時点では、どちらもドキュメントに記載はありません。たぶん、今後も無いのではないかと思われます。
参考:GitHub [8.x] Add ValidatorAwareRule interface
シナリオ
少し前に、某ワクチン予約システムで、存在しない市区町村コードでも予約できてしまうと、話題になりました。そこで今回は、都道府県名と市区町村名(コードではなく)をテキストに入力してもらい、市区町村名が実在するかをチェックするというカスタムルールを上記の2つのインターフェースを使いつつ、実現して行きたいと思います。
市区町村チェックは、「埼玉県」で「千代田区」とかも、もちろんダメとします。
早速作成
市区町村名を管理するテーブルを作る為、次のコマンドを打ち、モデルとマイグレーションファイルを作成します。
ついでにカスタムルールを作成するコマンドも打ちます。
php artisan make:model City -m
php artisan make:rule CityCheck
作成されたマイグレーションは、以下のようにします。
public function up()
{
Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->string('pref');
$table->string('name');
$table->timestamps();
});
}
DatabaseSeeder は、以下とします。(取りあえずここでは、3都道府県と5市区町村のみ)
public function run()
{
\DB::table('cities')->insert([
['pref' => '東京都', 'name' => '千代田区'],
['pref' => '東京都', 'name' => '中央区'],
['pref' => '埼玉県', 'name' => 'さいたま市'],
['pref' => '埼玉県', 'name' => '川越市'],
['pref' => '千葉県', 'name' => '市川市'],
]);
}
web.php は、以下とします。
<?php
use App\Rules\CityCheck;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::post('/', function (Request $request) {
$validated = $request->validate([
'pref' => ['required', 'string', 'in:東京都,埼玉県,千葉県'],
'city' => ['required', 'string', 'max:20', 'bail', new CityCheck()],
]);
dd($validated);
});
welcome.blade.php は、以下の感じ。
@if($errors->any())
<ul style="color: red">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<form method="post">
@csrf
都道府県:<input name="pref" value="{{ old('pref') }}">
市区町村:<input name="city" value="{{ old('city') }}">
<input type="submit" value="送信する">
</form>
問題の CityCheck は、以下の感じ。
<?php
namespace App\Rules;
use App\Models\City;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
class CityCheck implements Rule, DataAwareRule, ValidatorAwareRule
{
private $data;
private $validator;
public function setData($data)
{
$this->data = $data;
return $this;
}
public function setValidator($validator)
{
$this->validator = $validator;
return $this;
}
public function passes($attribute, $value)
{
if ($this->validator->errors()->has('pref')) {
return true;
}
$pref = data_get($this->data, 'pref');
return City::where('pref', $pref)->where('name', $value)->exists();
}
public function message()
{
return 'その市区町村は、その都道府県には存在しません。';
}
}
今回の validation では、
(1) 都道府県で既にエラーが出ている時は、DBを使った市区町村存在チェックは行わない、とし、
(2) また、市区町村についても市区町村の他でエラーが出ている時(市区町村名が100文字の時とか)は、同じくDBチェックは行わないという仕様にします。
(1) を実現する為に、ValidatorAwareRule というインターフェースを実装し、setValidator() メソッドを定義しています。これで、フレームワーク側が自動でこれを呼び出してくれ、validator オブジェクトを取得できますので、passes() メソッドの最初で、prefでエラーが発生しているかチェックして、発生していれば、true を返して終わりにしています。
もしエラーが発生していない時は、都道府県の値を取得する為に、DataAwareRule インターフェースを実装し、setData() メソッドを定義しています。
これでフレームワーク側がこのメソッドを呼び出してくれ、validation の対象となっている全データを取得することができますので、これに対し data_get() ヘルパー関数を使って、都道府県名だけを取得しています。
最後に、DBに存在確認して、その結果を return しています。
(2)についても(1)と同様の方法で実現できますが、今回は元のルール側に 'bail' を付けることで実現しています。これを付けることで、既に市区町村でエラーが出ていれば、DBチェックを走らせないようにする事ができます。(ちなみに、値が空の時(null)は、'bail'が無くても走りませんね)
感想
どちらも地味に便利そうです。
おかしな箇所等ありましたら、コメント下さい。
Discussion