You are here
Home > Author: ymikome

バリデーション (9) 配列のDistinctと暗黙の拡張

配列の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ではないなど)ニーズがあるかもしれませんね。

バリデーション (8) 配列をバリデーションする

laravel 5.2 で、バリデーションルールに配列を表す .* というプレースホルダーが使えるようになりました。

例えば、次のような商品オプションのバリデーションを考えます。

option_name

入力行の追加UIやドラッグ&ドロップによるソートはjQueryなどで実装することにします(laravel から離れるので解説は省きます)

この場合、項目数がいくつになるかわからないので、エレメントの属性名を name="option_name[{{$id}}]" などとし、配列を返すように作りますよね。

リクエストは次のようなものになるでしょう。配列のキーは編集データに依存しますので不定です(ここでは仮に 11, 12, 13 としました)

最低必要なバリデーションとしては、「保存」が少なくとも1つチェックされることと、「保存」がチェックされた行は必須入力となること、そして「販売単価」と「在庫数」の数値判定です。

苦労していた配列のバリデーションですが、5.2 からは次のようなルール定義が可能になったのです。

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

$messages = [
  'option_id.required' => '保存するレコードを少なくとも1つ選択してください',
];

「オプション名」、「販売単価」、「在庫数」のルール定義には属性名に .* を加えることで、配列の値それぞれにバリデーションが適用されるようになります。

「保存」がチェックされた行だけ入力が必要なので、required_with の引数も option_id.* となります。一方で「保存」の必須チェックは配列全体の required です。

配列部分のエラーは array_dot() による配列ドット記法で返ってきますので、Bladeテンプレートのエラーメッセージ表示は次のような書き方になります。

{{ $errors->first("unit_price.$id") }}

 

laravel 4 や 5.1 以前での配列バリデーション

後出しジャンケンでの自慢話となってしまいますが、私たちのプロジェクトでは lavavel 4 のころから配列に対するバリデーションを、5.2 と同じルールの書き方で処理してきました。

もともと laravel のバリデーションでは、リクエストは配列のまま処理されるでのはなく、array_dot によるドット記法の一次元データに変換されて内部処理されていました。

例えば次のように、配列のキーが固定されてる入力フォームの場合ならば、

<input type="text" name="name[last]" value="{{ old('name.last') }}">
<input type="text" name="name[first]" value="{{ old('name.first') }}">
Array
(
     [name] => Array
        (
            [last] => 山田
            [first] => 太郎
        )
)

以下のようにドット記法でルールを定義することで、配列データもバリデーションできたのです。

$rules = [
  'name.last'  => 'required',
  'name.first' => 'required',
];

 

最初に、「5.2 で .* というプレースホルダーに対応した」と書き、配列バリデーションそのものに対応したと書かなかった意味がここにあります。
このプレースホルダーの変換を自分で対応すれば、5.1 以前や 4 でも同様の処理ができるわけです。

定義するルール:

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

変換後のルール:

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

この変換は次のような関数で対応できます。ベースコントローラかトレイトに入れて、バリデーション直前にルールの変換を挿入してみてください。

/**
 * 配列バリデーションルールの変換
 * @param \Illuminate\Http\Request $request
 * @param array                    $rules
 * @param array                    $messages
 */
public static function setArrayRules($request, &$rules, &$messages)
{
    foreach($rules as $field => $rule)
    {
        // field文字列は foo.* か?
        if (!preg_match('/^(.+)\.\\*$/', $field, $m)) continue;

        // fooはリクエストに存在して配列か?
        $name = $m[1];
        if (!($req = $request->get($name)) || !is_array($req)) continue;

        foreach(array_keys($req) as $i)
        {
            // rulesに foo.$i を複写
            $rules["$name.$i"] = str_replace('*', $i, $rule);

            // messagesにfoo.$i.barがあれば複写
            foreach($messages as $key => $message)
            {
                if (!preg_match("/^$name\.\\*\.(.+)$/", $key, $m)) continue;
                $messages["$name.$i.{$m[1]}"] = $message;
            }
        }
        unset($rules[$field]);
    }
}

 

p.s.

上で紹介した setArrayRules() では、次のような配列の記述には対応してません。もし必要とするならばご自身で考えてみてくださいね。

Bladeソース:

<input type="text" name="option[{{$id}}][name]" value="old("opton.$id.name")">

バリデーションルール:

 'option.*.name' => 'required_with:option_id.*',

バリデーション (7) エラーメッセージのリプレーサー

等号を含む日付の最大最小

日付の最大最小 after, before の比較はなぜか等号を含みません。英単語の意味を厳格にプログラム仕様に落とし込んだようですが、実際の使い勝手としては等号を含んでほしかったところです。

これを等号を含むように拡張してしまうと、前回の min, max を拡張したのと違って意味が変わってしまいます。
そこで、start, end を新たに作成して追加してみましょう。

'start_date' => 'date|start:today',      // 今日を含んでそれ以降
'end_date'   => 'date|start:start_date', // 開始日を含んでそれ以降

最低限の追加で済むように、内部で標準バリデーションの after, before を呼び出すようにします。標準が等号を含まないのですから、普通に考えると after に与えられた引数を -1(秒)し、before の引数を +1(秒)するのがわかりやすいでしょう。

しかし、これらの引数は日付そのものだけではなく、上に示したように比較対象の属性名が与えられることがありますから、加減算をするには属性名から参照先の日付を取得するコードを自分で書かなければなりません。

そこで発想を逆転して、検査する入力値の日付を +1(秒)して after に、-1(秒)して before に与えることにします。意味を考えると分かりづらいですが、相対的なものなので正負を逆転しただけです。

作成したバリデーションルールは次のとおりです。

public function validateStart($attribute, $value, $parameters)
{
    $value = date('Y-m-d H:i:s', strtotime($value.' +1 sec'));

    return parent::validateAfter($attribute, $value, $parameters);
}

public function validateEnd($attribute, $value, $parameters)
{
    $value = date('Y-m-d H:i:s', strtotime($value.' -1 sec'));
/*
    // 比較相手が 日付のみ場合は 23:59:59を補完する
    if ((($date = $this->getValue($parameters[0])) || ($date = $parameters[0]))
        && !preg_match('/:/', $date))
    {
        $parameters[0] = date('Y-m-d 23:59:59', strtotime($date));
    }
*/
    return parent::validateBefore($attribute, $value, $parameters);
}

validateEnd() のコメントされたコード部分は、DatetimeDate を比較したときに起きる問題の補正です。

例えば、DBレコードの販売期間が Datetime で、出荷期間が Date のときなど、異なる日付型の間で比較が必要な場合には有効にしてください。
補正は end のみに必要で、start には必要ありません。

$tests = [
    ['2016-10-10', '2016-10-10 10:00:00'],
    ['2016-10-10 10:00:00', '2016-10-10'],
];
foreach($tests as $i => $date) {
    printf("%d => %d %d\n", 
    $i, ($date[0] >= $date[1]), ($date[0] <= $date[1]));
}

結果

0 => 0 1 // ('2016-10-10' >=' 2016-10-10 10:00:00') == FALSE は正しい!
1 => 1 0 // ('2016-10-10 10:00:00' <= '2016-10-10 23:59:59'に補正が必要

 

メッセージへの引数の展開

さて、前回、比較対象の属性名を引数に持てるように拡張した min, max において、エラーメッセージの引数部分が、バリデーション言語ファイルの定義通りの日本語に展開されませんでした。
今回追加作成した start, end にいたっては、英文字の属性名にすら展開されません。この問題を解決しましょう。

今、バリデーション言語ファイルが次のように定義されているとします。

resources/lang/ja/validation.php

'after'  => ':attributeが:dateより前',
'before' => ':attributeが:dateより後',
'max' => [
  'numeric' => ':attributeが:maxよりも大きい',
],
'min' => [
  'numeric' => ':attributeが:minよりも小さい',
], 

// 追加

'start'  => ':attributeが:dateより前',
'end'    => ':attributeが:dateより後',

// 項目名

'attibutes' => [
  'end_date'   => '終了日',
  'start_date' => '開始日',
  'unit_cost'  => '仕入単価',
  'unit_price' => '販売単価',
],

このとき、それぞれのルールに対するエラーメッセージは次のように展開されます。

区分 ルール エラーメッセージ 課題
標準 ‘unit_price’ => ‘min:0’ 販売単価が0より小さい
拡張 ‘unit_cost’ => ‘max:unit_price’ 仕入単価がunit_priceより大きい 日本語にはならない
標準 ‘end_date’ => ‘before:start_date’ 終了日が開始日より後
追加 ‘end_date’ => ‘end:start_date’ 終了日が:dateより後 属性名への展開すらない

属性名を引数に取れるように拡張した min, max で英文字の属性名が展開できるのは、数値が与えられたときの標準の展開がそのまま適用されているためです。
 

カスタムプレースホルダー

エラーメッセージの中の :attribute:min, :date はプレースホルダーと呼ばれます。

カスタムのプレースホルダーを追加するには、サービルプロバイダーの boot メソッドの中で、Validator::replacer() メソッドを呼び出します。

Validator::replacer('foo', function($message, $attribute, $rule, $parameters) {
    return str_replace(...);
});

もしくは、Validator を継承した CustomVlidator クラスでは、addReplacer() メソッドか、複数のリプレーサーを配列にしてまとめて登録できる addReplacers() メソッドで登録できます。

$this->addReplacer('foo', function($message, $attribute, $rule, $parameters) {
    return str_replace(...);
});

リプレーサーの中身は簡単な文字列置換です。

与えられたエラーメッセージ($message)に対して、プレースホルダー文字列(":min"":date" など)を引数の文字列($parameters[0])に置き換えるコードを記述します。

引数($parameters[0])は与えられた最小値の値や、比較相手の英文字の属性名です。これを日本語に展開するには、バリデーション言語ファイルの $attributes 配列から対応語を参照する getAttribute() メソッドを使います。

 

以下、CustomValidator クラスのコンストラクタで min, max, start, end にリプレーサーを登録するサンプルです。共通部分が多いのでクロージャを変数に代入し、setReplacers() でまとめて配列登録しました。

これで、エラーメッセージは期待通りすべて日本語に展開されるようになりました。

<?php

namespace App\Services;

class CustomValidator extends \Illuminate\Validation\Validator
{
    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // 汎用(ルールと同名)
        $replacer = function($message, $attribute, $rule, $parameters) {
            return str_replace(':'.$rule, $this->getAttribute($parameters[0]), $message);
        };
        // 日付
        $date = function($message, $attribute, $rule, $parameters) {
            return str_replace(':date', $this->getAttribute($parameters[0]), $message);
        };

        $this->addReplacers([
            'min' => $replacer, 'max' => $replacer, 'start' => $date, 'end' => $date
        ]);
    }

バリデーション (6) 他の属性の値を参照

日付の最大最小 after, before では、引数に値(日付)を与えるだけでなく、比較対象の属性の名前を与えることができます。
むしろ具体的な日付を引数にすることのほうがまれでしょう。

'date_start' => 'date|before:today',
'date_end'   => 'date|after:date_start',

このような使い方は、数値型の最大最小 min, max でも使用したいこともありますよね?

例えば、次のような関係です。

unit_cost

'unit_price' => 'required|integer|min:0',
'unit_cost'  => 'required|integer|min:0|max:unit_price',

標準バリデーションの minmax を拡張して、比較対象の属性名を引数に持てるようにしてみましょう。

 

getValue()

バリデーションクラスにルールを登録するときの雛形は次のようなものでした。
一見すると引数には、ルールを定義した属性に関する情報しか与えられません。

/**
 * 追加バリデーション
 * @param  string  $attribute  検査する属性名
 * @param  string  $value      入力された値
 * @param  array   $parameters 引数の配列
 * @return boolean
 */
public function validateHoge($attribute, $value, $parameters)
{
  //
}

しかし、バリデーションクラスにはリクエストされたすべての属性に関する情報が与えれていて、他の属性に入力された値も getValue() で得ることができます。getValue() の引数は属性名です。

よって、次のバリデーションは必ず TRUE を返します。

public function validateHoge($attribute, $value, $parameters)
{
   return ($value == $this->getValue($attribute));
}

 

min と max の拡張

min, max の引数は、属性の型が何であろうと必ず数値を取ります。

そこで、引数が数値(is_numeric)でないときを限定し、引数を属性名とした値が存在するならそれを新たなしきい値としました。

/**
 * 最小値 min
 * @param string $attribute
 * @param string $value
 * @param array $parameters	0 => 比較する属性名
 * @return true
 */
protected function validateMin($attribute, $value, $parameters)
{
    if (!is_numeric($parameters[0]) && 
        !is_null($val = $this->getValue($parameters[0])))
    {
        $parameters[0] = $val;
    }
 
    return parent::validateMin($attribute, $value, $parameters);
}

/**
 * 最大値 max
 * @param string $attribute
 * @param string $value
 * @param array $parameters	0 => 比較する属性名
 * @return true
 */
protected function validateMax($attribute, $value, $parameters)
{
    if (!is_numeric($parameters[0]) &&
        !is_null($val = $this->getValue($parameters[0])))
    {
        $parameters[0] = $val;
    }
 
    return parent::validateMax($attribute, $value, $parameters);
}

 
* * * *

さて、拡張した最大最小バリデーションですが、このままではエラーメッセージが与えられた引数の属性名のままとなり、言語ファイルによる置き換えが行われません。

unit_cost2

本来の最大最小が与えられた引数そのものを、メッセージのプレースホルダー :max と置き換えることしか想定していないからです。
この部分の解決については次回にまとめる予定です。

バリデーション (5) 最大値と最小値

バリデーション言語ファイルを編集するときにお気づきと思いますが、Laravel標準バリデーション MaxMin は(他に SizeBetween も)調査対象の属性の型でその挙動を変えます。

'max' => [
    'numeric' => ':attributeの値が:maxを超えています',
    'file'    => ':attributeのサイズが:max kBを超えています',
    'string'  => ':attributeの文字数が:maxを超えています',
    'array'   => ':attributeの個数が:maxを超えています',
],
'min' => [
    'numeric' => ':attributeの値が:minに足りません',
    'file'    => ':attributeのサイズが:min kBに足りません',
    'string'  => ':attributeの文字数が:minに足りません',
    'array'   => ':attributeの個数が:minに足りません',
],

ここで問題となるのは、文字列型が入力した文字数カウントとの比較になることです。文字列比較の大小によるバリデーションは存在してません。

例えば日付型の属性に対する開始日のつもりで Min を指定しても、文字数判定となり、期待したバリデーションにはなりません。

'date_start' => 'date|min:'.date('Y-m-d'),    // 開始日判定にはならない

 

日付の最大値と最小値

日付型の属性の前後関係の判定には、専用の afterbefore が用意されています。
しかも、引数の文字列は strtotime で処理されるので次のような指定が可能です。

'date_start' => 'date|after:today',    // 今日より大(明日以降)
'date_end'   => 'date|befor:+1 year',  // 1年以内

または属性名を引数に持つことができるので、期間指定のバリデーションに使うことが出来ます。

'date_end'   => 'datetime|after:date_start',

invalid_period

ただし、after, befor の比較は等号を含みませんので注意が必要です。

 

数値型のカスタムバリデーション

例えば1以上の整数(自然数)を判定するカスタムバリデーション natural_number を作成した場合、それを minmax で大小判定したときは、正しく数値として判定されるでしょうか?

結論を言えば、カスタムバリデーション natural_number は(そのままでは)、入力された値が数値であっても、文字列として判断され、文字数でカウントされてしまいます。

'quantity' => 'requied|natural_number|max:10', // 10桁( 10億)まで可

 

では、数値として認められるにはどのようにしたら良いのでしょうか。

vender の Validation.php でコードを追うと、getSize() メソッドにおいて次のように型判定されていることがわかります。

1 入力値が数値(is_numeric)でバリデーションに numericinteger があれば、その値を返す
2 入力値が配列(is_array)なら配列の個数を返す
3 入力値が File インスタンスならファイルサイズを kB で返す
4 それ以外は文字数(mb_strlen)を返す

数値として判定されるには、入力された値が数値であるかどうかだけでなく、バリデーションに numericinteger が含まれなければなりません。これでは他のどんなルールであっても数値と判定されることはないのです。

 

numericRules

この数値と判断されるためのバリデーションルールは、変数 $numericRules で定義されていました。

protected $numericRules = ['Numeric', 'Integer'];

カスタムバリデーションとして数値型のルールを自作した場合には、これにルール名を追加すればよいわけです。

変数 $numericRulesprotected で再定義して上書きするか、コンストラクタで必要分だけ追加します。……再定義する場合は NumericInteger を忘れずに。

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

    $this->numericRules[] = 'NatrualNumber';
}

/**
 * 自然数 natural_number
 *
 * @param string $attribute		検査する属性名
 * @param string $value			検査する値
 * @return bool
 */ 
public function validateNaturalNumber($attribute, $value)
{
    if ($this->validateInteger($attribute, $value))
    {
        return ($value > 0);
    }

    return false;
}

バリデーション (4) DateとDatetime

Laravel 標準の Date バリデーションは、次のときに TRUE を返します。

OR 値がPHPの DateTime クラスのインスタンスである
AND 値が strtotime で理解できる文字列である
date_parse で年月日を返す
checkdate で年月日が妥当である

文字列の具体例でまとめると以下のようになります。

文字列 バリデーション MySQL保存
例01 now FALSE
例02 +1 day FALSE
例03 next Thursday FALSE
例04 2016-09-30 + 1day TRUE ×
例05 30 September 2016 TRUE ×
例06 2016-09-30 TRUE
例07 20160930 TRUE
例08 2016/09/30 TRUE
例09 09/30/2016 TRUE ×
例10 09-30-2016 FALSE
例11 30/09/2016 FALSE
例12 2016-09-30 10:10:00 TRUE

date_parse を通すため 例01〜例03のような論理指定は弾かれますが、例04のような記述では前半部分が年月日の配列値を返すためバリデーションは通ります。
また、アメリカ式の月日年(例09)が通って、ヨーロッパ式の日月年(例11)は弾かれます(これは環境のタイムゾーンに影響されるでしょう)。

いずれにせよ、バリデーションに通ってもそのままデータベースに保存できるとは限りません。
データベース保存を前提にするなら、ISO-8601 の "YYYY-MM-DD" 書式に限定したほうが何かと都合が良いですね。

 

date_format による ISO-8601限定

入力され日付のフォーマットを限定するために date_format が用意されています。引数は date_parse_from_format を通して処理されます。
datedate_format は両方を同時に使用することはできません。

'date'     => 'date_format:Y-m-d',
'datetime' => 'date_format:Y-m-d H:i:s',

 

Date と Datetime の拡張

データベースの保存フィールドで DateDatetime を区別し、入力フォームのインターフェースもこの2つしかないならば、date_format を使用せずに、date バリデーションを拡張してしまうこともできます。

書式を正規表現で限定してからオリジナルのバリデーションを通せばよいでしょう。

    /**
     * date (without datetime)
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    protected function validateDate($attribute, $value)
    {
        // YYYY-MM-DD に限定する
        if (!preg_match('/^[0-9]+-[0-9]+-[0-9]+$/', $value)) return false;

        return parent::validateDate($attribute, $value);
    }

    /**
     * datetime
     *
     * @param string $attribute
     * @param string $value
     * @return bool
     */
    public function validateDatetime($attribute, $value)
    {
        // YYYY-MM-DD hh:mm に限定する
        if (!preg_match('/^[0-9]+-[0-9]+-[0-9]+ ([0-9]+):([0-9]+)/', $value, $m)) return false;

        return parent::validateDate($attribute, $value);
    }

validateDete() は標準の置き換えなので protected 宣言ですが validateDatetime() は追加関数として public で宣言しています。

 

日付入力のフォームインターフェイス

入力バリデーションとして DateDatetime を用意したならば、入力フォームもそれに対応しなければなりませんが、HTML5 の日付フォームはブラウザの対応に依存し、非対応な環境を無視したとしてもユーザーインターフェイスが人によって異なりサポートが難しいものです。

私たちのプロジェクトの管理画面では、Bootstrapベースの SmartAdmin を導入してインターフェイスの統一を図っています。

SmartAdmin の日付フォームは jQuery UI DatepickerBootstrap Timepicker の組み合わせを採用していますが、HTML5の datetimedatetime-local には対応してません。スクリプトを書くことで datetime 属性(厳密には datetime-local 属性)の入力フォームを実現する必要があります。

datetime

bladeソース(の概念)と対応するjQueryスクリプト

<div class="input-group-datetime">
  <input type="hidden" name="date_start" value="{{ old('date_start') }}">
  <input class="datepicker">
  <input class="timepicker">
</div>
$('.input-group-datetime').on('change', 'input:visible', function() {
  // 日付または時刻が変更された
  var $parent = $(this).parents('.input-group-datetime'),
    dt = $parent.find('.datepicker').val(),
    tm = $parent.find('.timepicker').val();
  if (dt && !tm) {
    // 時刻が空なら00:00で補完
    tm = '00:00';
    $parent.find('.timepicker').val(tm);
  }
  if (!dt) {
    // 日付が空なら時刻も削除
    $parent.find('.timepicker').val('');
  }
  // 日時を合成して値を格納
  $parent.find(':hidden').val((dt) ? dt + ' ' + tm + ':00' : '');
});

バリデーション (3) 既存バリデーションの置き換え

Laravelには最初から利用可能なバリデーションルールが多数存在していますが、そのいくつかは実用にならなかったり、自分好みに挙動を変えたりしたいこともあります。

CustomValidator クラスは標準 Validator を継承していますので、これらを置き換えて上書きすることが可能です。

vendor/laravel/framework/src/Illuminate/Validation/Validator.php

ここから関数宣言をコピーして、前回作成した CustomValidator クラスのファイルに貼り付けて編集します。引数はそれぞれの関数によって異なりますし、宣言が public ではなく protected であることに注意してください。

 

email

標準の email バリデーションは実在してるメールアドレスがエラー判定になることがあります。これを解決するために正規表現による判定に置き換えてみたものです。

    /**
     * email
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    protected function validateEmail($attribute, $value)
    {
        return (preg_match("/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,10})$/i", $value));
    }

 

管理画面では、メール送信者の指定などで名前付きメールアドレスの入力を容認する場面もあります。ついでに email_with_name を作成しておきましょう。

クラスメソッドとして関数を書いていますので、内部で他のバリデーションルールを呼び出して使用することができます。
ここでは先に上書きした email を使用するので $this->validateEmail() で呼び出していますが、標準ルールを呼び出すなら parent::validateEmail() となります。

    /**
     * email_with_name
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    public function validateEmailWithName($attribute, $value)
    {
        if (preg_match('/<(.*)>$/', $value, $m)
        {
            $value = $m[1];
        }

        return $this->validateEmail($attribute, $value);
    }

 

alpha, alpha_dash, alpha_num

これらアルファベット系の標準バリデーションは、全角英数字の文字コードを通します。

半角のasciiコードだけの入力許可に絞るため、これらも正規表現によるバリデーションルールに置き換えます。

/**
 * alpah
 *
 * @param string $attribute
 * @param string $value
 * @return true
 */
protected function validateAlpha($attribute, $value)
{
    return (preg_match("/^[a-z]+$/i", $value));
}

/**
 * alpah_dash
 *
 * @param string $attribute
 * @param string $value
 * @return true
 */
protected function validateAlphaDash($attribute, $value)
{
    return (preg_match("/^[a-z0-9_-]+$/i", $value));
}

/**
 * alpah_num
 *
 * @param string $attribute
 * @param string $value
 * @return true
 */
protected function validateAlphaNum($attribute, $value)
{
    return (preg_match("/^[a-z0-9]+$/i", $value));
}

ただし、ユーザが入力した全角英数に対してただエラーを返すようなユーザーインターフェースは褒められたものではありませんね。

入力を受け付けてからphpで半角に変換するという方法もありますが、私たちはjQueryスクリプトで入力前に変換する方法を選択しています。

jQueryスクリプトの例:

			var toHankaku = function (strVal){
				// 半角変換
				var halfVal = strVal.replace(/[!-~]/g, function(tmpStr) {
					// 文字コードをシフト
					return String.fromCharCode(tmpStr.charCodeAt(0) - 0xFEE0);
				});

				// 文字コードシフトで対応できない文字の変換
				halfVal = halfVal.replace(/”/g, "\"")
					.replace(/[ーー―-‐]/, "-")
					.replace(/’/g, "'")
					.replace(/‘/g, "`")
					.replace(/¥/g, "\\")
					.replace(/ /g, " ")
					.replace(/?/g, "~");

				return halfVal;
			};

			$("input.en").change(function() {
				var $this = $(this);
				$this.val(toHankaku($this.val()));
			});

バリデーション (2) CustomValidatorを追加

カスタムバリデーションのクラスを追加するには、サービスプロバイダーで次のようにクラスを登録します。

追加するクラスの名前や位置はどのようなものでもかまいせん。ここでは、ディレクトリパス app/Services に CustomVaidator.phpを作成し、AppServiceProvider に登録することにします。

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;
 
use Validator;
use Illuminate\Support\ServiceProvider;
use App\Services\CustomValidator;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
         Validator::resolver(function($translator, $data, $rules, $messages)
        {
            return new CustomValidator($translator, $data, $rules, $messages);
        });
    }
 
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

 

次に、標準の Validator を継承する CustomValidator を作成します。
前回 AppServiceProvier.php に直接登録したバリデーションルールを、こちらに移します。

app/Service/CustomValidator.php

<?php

namespace App\Services;

class CustomValidator extends \Illuminate\Validation\Validator 
{
    /**
     * kana
     *
     * @param string $attribute
     * @param string $value
     * @return bool
     */
     public function validateKana($attribute, $value)
     {
        // かな、半角空白、全角空白、全角記号を許可
        return preg_match("/^[ぁ-んー  !-@[-`{-~]+$/u", $value);
     }
}

こうして専用のバリデーションクラスを作成すると、それぞれのバリデーションルールは通常のメソッドとして実行できるようになり、ユニットテストの作成も容易になります。

$bool = CustomValidator::validateKana('kana', $request->get('kana'));

 

前回との重複になりますが、エラーメッセージをバリデーション言語ファイルの「Validation Language Lines」グループに追加します。

'unique' => ':attribute に指定された値はすでに存在しています',
'url'    => ':attribute のフォーマットが正しくありません',
 
// カスタムバリデーション
 
'kana'   => ':attribute は全角のひらがなで入力してください',

これでふりがなバリデーションをどこでも利用できるようになりました。

'name_kana' => 'required|kana',

次回から、このクラスにいろいろなルールを登録していきましょう。

 

ところで、ふりがなに「カタカナ」を強要するサイト設計が多いことに苛立つのは私だけでしょうか?
無駄にカタカナを入力するとIMEの辞書学習が汚れ、その後の漢字変換のリズムが崩れるのです。名簿などの入力作業をすると誰でも体験できるでしょう。

カタカナルールは昔ながらの紙文化の遺物なのでしょう。手書きならばそのほうが識別しやすいに違いないですが、電子データにその配慮は必要ありませんよね。

バリデーション(1)Validatorファサードのextend

はじめまして。ブログ主筆khino氏と同じプロジェクトで仕事をしてます。
彼とは別テーマを平行して掲載しますので、これまで順番に読み進めていた方にはちょっと読みにくくなるかもしれませんが、ご容赦ください。

私の最初のテーマはカスタムバリデーションルールです。
このシリーズでは、Validatorファサードの基本的な使い方から始めて、より複雑なルールの定義の仕方や、laravel 5.2で追加された配列定義のカスタムバリデーションまで紹介する予定です。

まずは、Validatorファサードのextendを利用する方法から。

コントローラやモデルに依存しない一般的なルールの追加は、サービスプロバイダーの中で定義するのが一般です。

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Validator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
         Validator::extend('kana', function($attribute, $value, $parameters, $validator) {
            // 半角空白、全角空白、全角記号、全角かなを許可
            return preg_match("/^[ぁ-んー  !-@[-`{-~]+$/u", $value);
        });    
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

例は全角ひらがなだけを許可するバリデーションですが、見たとおり正規表現による判定を行っています。
カスタムバリデーションを使いこなす前に、正規表現を使いこなさなければなりませんが、それはまた別に勉強してくださいね。

エラーメッセージはバリデーション言語ファイルに追加しておきましょう。追加するのは「Validation Language Lines」のグループの末尾です。

resouces/lang/ja/validation.php に追加

    'unique' => ':attribute に指定された値はすでに存在しています',
    'url'    => ':attribute のフォーマットが正しくありません',

    // カスタムバリデーション

    'kana'   => ':attribute は全角のひらがなで入力してください',

「Custom Validation Language Lines」のグループは、属性名とルールの組み合わせでメッセージを独自のものにすると言う意味での「カスタム」です。

    'custom' =>[
        'password'  => [
            'alpha_num' => 'パスワードは10文字以上40文字以下の英数字で入力してください',
            'between'   => 'パスワードは10文字以上40文字以下の英数字で入力してください',
        ],
    ],

また、言語ファイル末尾の「Custom Validation Attributes」に属性名の訳を追加しておきましょう。メッセージ内の「:attribute」の部分に用いられるようになります。

    'attributes' =>[
        'name'  => '名前',
        'email' => 'メールアドレス',
    ],

 

また、モデルやコントローラに依存して、そこでしか使用しないバリデーションなら、次のように フォームリクエスト で定義することも可能です。

しかし、この位置ではユニットテストが作りにくくなるので、次回で説明するようにカスタムバリデーションクラスを作成して、すべてのバリデーションを1つにまとめて管理するほうが良いように思います。

app/Http/Requests/MemberRequest.php

<?php

namespace App\Http\Requests;

use Validator;
use App\Http\Requests\Request;
use App\Member;

class MemberRequest extends Request
{
    public function authorize() {
        return true;
    }

    public function rules()
    {
        Validator::extend('kana', function($attribute, $value, $parameters, $validator) {
            // 半角空白、全角空白、全角記号、全角かなを許可
            return preg_match("/^[ぁ-んー  !-@[-`{-~]+$/u", $value);
        });

        $member = Member::find($this->member_id);

        switch($this->method())
        {
        case 'POST':
            return [
                'email' => 'required|email|unique:member,email',
                'password'  => 'required|min:6|max:20|confirmed',
                'name' => 'required',
                'kana'  => 'required|kana'
            ];

        case 'PUT':
        case 'PATCH':
            return [
                'email' => 'required|email|unique:member,email,'.$member->member_id.',member_id',
                'name' => 'required',
                'kana'  => 'required|kana'
            ];
        }
    }

    public function attributes()
    {
        return [
            'email'	=> 'メールアドレス',
            'password' => 'パスワード',
            'name' => '名前',
            'kana' => 'ふりがな'
        ];
    }

    public function messages()
    {
        return [
            'kana'	=> ':attribute は全角ひらがなで入力してください',
        ];
    }
}
Top