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

日付の最大最小 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
        ]);
    }

By ymikome