疑問
FormRequestクラスを継承した自前のクラスで、以下のようなルールを定義した。
public function rules()
{
return [
'id' => [
'required',
"integer",
"between:1,2147483647",
'exists:users,id',
];
}
このidに対して、2147483648を入力しても、データベースエラーにはならない。
また、文字列 "abcd" などを入力しても、やはりデータベースエラーにはならない。
existsより手前に設定されている検証がNGの場合に、exists検証が実行されていないようだ。
なぜ?
そういえば、requiredが失敗するとそれ以降のバリデーションが実行されていないのも理由がよくわからない。
https://readouble.com/laravel/5.5/ja/validation.html
ドキュメント中に、「暗黙の拡張」という項目があって、ここが関係していそうなのだが、ドキュメントを読んだだけでは要領を得ない感じ。
実験
そこで、自作のバリデーションルール(my_rule_exists_in_users)を作って、existsの代わりに適用してみた。
※このmy_rule_exists_in_usersは、usersテーブルのid列に、入力値が存在しているかの確認を行う処理を実装。
public function rules()
{
return [
'id' => [
'required',
"integer",
"between:1,2147483647",
//これを自作してみた
'my_rule_exists_in_users',
];
}
この場合、2147483648や"abcd"を入力すると、integer型に対して不適切な入力値であるためデータベースエラーが発生する。
なんでexistsはうまいこと動いているの?
解決編
Illuminate\Validation\Validator::validateAttribute() および、
Illuminate\Validation\Validator::isValidatable()
の実装を見ると、なせこのような動きになっているのかがわかった。
/**
* Determine if the attribute is validatable.
*
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
*/
protected function isValidatable($rule, $attribute, $value)
{
return $this->presentOrRuleIsImplicit($rule, $attribute, $value) &&
$this->passesOptionalCheck($attribute) &&
$this->isNotNullIfMarkedAsNullable($rule, $attribute) &&
$this->hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute);
}
/**
* Determine if it's a necessary presence validation.
*
* This is to avoid possible database type comparison errors.
*
* @param string $rule
* @param string $attribute
* @return bool
*/
protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute)
{
return in_array($rule, ['Unique', 'Exists']) ? ! $this->messages->has($attribute) : true;
}
各バリデーションは、実際に検証を行う前に isValidatable() を呼んでいて、このメソッドがfalseを返すと検証自体をスキップする仕様になっているようだ。
検証ルールにrequired、nullableなどを追加している場合の動きも、この辺で実装されているっぽい。
exists の動作に直接関わっているのは↑の hasNotFailedPreviousRuleIfPresenceRule() で、ここに答えが書いてあった。
結論
自作のバリデーションルールに、existsルールのような「それより手前のバリデーションがNGの場合に検証自体をスキップする」よう振る舞ってほしい場合は、Validatorクラスを継承した自分用のValidatorクラスを定義して、hasNotFailedPreviousRuleIfPresenceRule()をオーバーライドすればよい、ということになりそう。
この['Unique', 'Exists'] を外から書き換えられるようにしておいてほしい感じではある。