uniqueバリデーションの拡張をしたら色々しんどかった

はじめに

Laravelの標準で用意されているuniqueルールのバリデータは、

データを1件ずつデータベースと突き合せるということをしないといけないため、

配列の要素に対して行うと要素数SQLを投げてパフォーマンス上よろしくないという問題があります。

今回はこのSQLぐるぐる問題を解決するためのuniqueの拡張バリデーションを書いてみました。

そうしたら、思ってたよりも遥かに難しかったし、uniqueバリデーションの中身もかなり読み込んだので備忘録として残しておきます。

作ったもの

  • チェックする対象データをデータベースから一括取得して、対象データが一意性制約違反していないかをチェックする
  • Illuminate\Validation\Rules\Uniqueを使ってEloquentライクに条件を組み立てる
  • INNER JOINに対応したため、テーブル跨がないと判らないものでもチェックできる

前提条件

  • PHP 7.x
  • Laravel 5.5

困ったこと

  1. $verifierをどうやって差し替えれば良いのか判らない

本家のvalidateUniqueの実装を見ると、

 return $verifier->getCount(
            $table, $column, $value, $id, $idColumn, $extra
        ) == 0;

上記の実装になっているので、これを先読みしたデータと突き合せれば良いことはすぐに判ります。

$verifier自体はブレイクポイントか何かで見れば、

Illuminate\Validation\DatabasePresenceVerifierクラスが実態であることはすぐに判るので中身を見ると、

getCount自体はインタフェースで与えられてしまっているので、DatabasePresenceVerifierクラス相当を自分で作って差し替えてあげないといけません。

ちなみに差し替えは、

PresenceVerifierInterfaceを指定されてしまっているので、これをimplementsもしてあげないといけない。

とりあえず、$verifierの差し替え方なんてどこにも載ってないので、コードを読むと

Illuminate\Validation\Validator::setPresenceVerifierで上書きできることが判るので、とりあえず自作クラスで上書きすることに成功。

ここまでで4時間・・・

  1. そもそもIlluminate\Validation\Rules\Unique相当のクラスを自作しても、Validatorに渡せない

何回やっても__toString()した値が渡されるので、実装を見てみたら以下の記載があって絶対に不可能になってた。

Illuminate/Validation/ValidationRuleParser.php

if (! is_object($rule) ||
    $rule instanceof RuleContract ||
    ($rule instanceof Exists && $rule->queryCallbacks()) ||
    ($rule instanceof Unique && $rule->queryCallbacks())) {
    return $rule;
}

こんな実装になってるので、Illuminate\Validation\Rules\Uniqueを継承してやり過ごす。。。

(幸いというか、Illuminate\Validation\Rules\Uniqueの拡張を作ろうとしてたので、変ではない)

ちなみに本件はLaravel 5.7だと

Illuminate/Validation/Rules/Conditionalクラスを継承することによって、任意のクラスを作れるように変わっています。

github.com

そしてここまでで6時間・・・

joinとか諸々追加して8時間くらいかかったのであった。。

まとめ

すぐ終わるだろとか思ったら、普通に1日かかったので、標準から外れたことをしようとするとやっぱりLaravelは難しい。

5.5でIlluminate\Validation\Rules\Uniqueとかに類似の機能は基本的に追加不可(勿論無理やり追加は可能)

追加する場合は、

  • 既存のRulesを継承
  • (必要なら)DatabasePresenceVerifier相当のものを自前で実装。ただし、PresenceVerifierInterfaceを実装する
  • Illuminate\Validation\Validator::setPresenceVerifierで上記のverifierを差し替える

5.7以上は拡張できるようになってるのでそれ使う。(ドキュメント化まだされてないっぽい)

めちゃくちゃ非公式のやり方してるので、コードは載せない・・・