配列のDistinctバリデーション

前回の例題とした商品オプションに、オプション名に同じ値が入ることを禁じるバリデーションを追加します。

distinct

これには、配列プレースホルダー .* ともに laravel 5.2 で追加された distinct バリデーションが利用できます。よく考えられていますね。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*|distinct',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

空の入力に対するdistinct?

ところで、ここまで「オプション名」は必須入力であることを前提としてきましたが、商品オプションが1つだけなら「オプション名」は空のまま「販売単価」と「在庫数」だけ入力できるようにしたい、という要望が現場から上がってくるかもしれません。

もう少し仕様追求すると、商品オプションが2つ以上であっても、先頭(とは限らないかもしれません)の1つは空でよいかもしれません。

このために空の入力が1個まで可というバリデーションルールを追加するとロジックが複雑化します。required_with を外したとき distinct が空文字列もあわせて重複チェックしてくれれば話が簡単ですね。こんなふうに・・・

distinct_with_blank

3つの入力欄のうち2つを空にした場合のリクエストは次のようになります。

Array
(
    [option_name] => Array
        (
            [11] => 
            [12] => 
            [13] => オレンジ
        )

option_name のキー 11 と 12 は同じ値(空文字列)なのだから重複チェックにかかりそうなものです。

しかしこのバリデーションはそんな都合よく機能しません。

 

distinct バリデーションは、option_name 全体ではなく、配列の値それぞれに働きます。キー 11 の値は配列内で重複してるか? キー 12 の値は・・・と。

そして、バリデーションの原則は値が入力された項目にのみ働きます。このケースでは、そもそもキー 11 と 12 は調査されることなく、キー 13 の値が配列内で重複してるかだけが調べられ、重複なしと判定されるのです。

そう、リクエストが空でも働くのは、required 系バリデーションだけでした。

 

暗黙の拡張 implicitRules

ここまでくると次の展開が読めましたか?

この課題は解決には、distinctrequired と同様に 空リクエストに対してもバリデーションが働く 必要があり、これを実現するのが 暗黙の拡張 ルールです。

この暗黙の拡張を「暗黙の必須」と読んでしまうと理解を間違えます。空リクエストに対するバリデーション、それ以上でも以下でもありません。

暗黙の拡張ルールを作るには、Validator::extendImplicit() メソッドを使います。通常の拡張ルールとして登録されると同時に、暗黙の拡張ルールの名前を収めた配列 $implicitRules にルール名が追加されます。

Validator::extendImplicit('foo', function($attribute, $value, $parameters, $validator) {
    return $value == 'foo';
});

ということは、Validator を継承して作成した customValidaor クラスでは、直接 $implicitRules にルール名を追加することで暗黙の拡張を作ることができます。

以下は、空文字列を含んだ重複判定ができる distinct_with_blank バリデーションの作成例です。

class CustomValidator extends \Illuminate\Validation\Validator {

    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // 暗黙の拡張に追加
        $this->implicitRules[] = 'DistinctWithBlank';
    }

    /**
     * 空文字列も含む重複判定 distinct_with_blank
     * @param  string $attribute
     * @param  string $value
     * @param  array  $parameters
     * @return true
     */
    public function validateDistinctWithBlank($attribute, $value, $parameters)
    {
        // 標準distinctを呼ぶだけ
        return parent::validateDistinct($attribute, $value, $parameters);
    }

これを用いて option_name.* から required_with を外すと、最初のルール定義は次のようになります。無事に、オプション名の空は1つだけ許可されるようになりました。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'distinct_with_blank',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

この他にも、DB上の重複をチェックする unique に対し、空入力でも機能する unique_with_blank も状況によっては(DB定義がNULL OKではないなど)ニーズがあるかもしれませんね。

By ymikome