1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DateTime::createFromFormat の月日が0から99まで入力できる問題とLaravelバリデーションから型の話

Posted at

概要

Laravel3のバリデーションルール'date_format:j'が0~99までの入力を許していたため調査した。
本来ならば「j」は日にちを指すので、1~31の入力であることを判定してほしい。

結果、話が簡単に終わらなくて泣いた。

結論

  • requiredbetweenctype_digitを使ったカスタマイズルール(+このルールをnumeric判定ルールに追加) 」の組み合わせを使う
  • または「requireddate_dormatbetweennumeric」の組み合わせを使う
  • いっそ正規表現で判定する
  • むしろまとめたカスマタイズルールを作成しろ

分解すると

  • 0~99までを受け入れるのは、恐らく仕様(PHP5.3の結果)
  • よって、日を判定するならば、別途between:1,31などが必要
  • ただし、between(min,max)などはデフォルトでは文字列の文字数なので、別途「入力値を数値とみなす」ルールが必要
  • 既存の「「入力値を数値とみなす」ルール」では数値文字だけを判定できない
  • 「「入力値を数値とみなす」ルール」では数値文字だけを判定できないので、見逃した数値以外の文字列を弾くdate_formatか、そもそも完全に数値文字だけを判定するctype_digit()を使用する

流れ

  1. Laravelバリデーションルールdate_formatの調査
  2. data_formatルールに使われるdate_create_from_formatの調査
  3. 0~99は仕様と受け止め、追加ルールbetween:1,31で収めようとした結果
  4. betweenルールに必要なfilter_varの調査
  5. filter_varでは不十分なので代用のis_numericの調査
  6. is_numericも不完全だけど、組み合わせで補完できるよね
  7. ctype_digitってもしかして完璧?今回は使わないけど

バリデーションの実装を見る

実装はLaravel\Validatorクラスのvalidate_date_format()
Laravel3のValidator.php

Validatorクラス
	protected function validate_date_format($attribute, $value, $parameters)
	{
		return date_create_from_format($parameters[0], $value) !== false;
	}

date_create_from_formatDateTime::createFromFormatのエイリアスである。
PHP: date_create_from_format - Manual

この関数は次の関数のエイリアスです。 DateTime::createFromFormat()

そして、DateTime::createFromFormat()の説明には

format 文字 説明 取りうる値の例
d および j 2桁の日付。先頭のゼロを含むものと含まないもの 01 から 31 あるいは 1 から 31

とある。
見かけ上は希望通りである。

動かしてみる

デバッグコード入り
        protected function validate_date_format($attribute, $value, $parameters)
        {
                \Log::debug("バリデーションチェック", true);
                \Log::debug($value, true);
                \Log::debug($parameters, true);
                \Log::debug(date_create_from_format($parameters[0], $value), true);
                \Log::debug('終了', true);
                return date_create_from_format($parameters[0], $value) !== false;
        }
入力値1
2015-04-23 16:14:35 DEBUG - バリデーションチェック
2015-04-23 16:14:35 DEBUG - 1
2015-04-23 16:14:35 DEBUG - Array
(
    [0] => j
)

2015-04-23 16:14:35 DEBUG - DateTime Object
(
    [date] => 2015-04-01 16:14:35
    [timezone_type] => 3
    [timezone] => Asia/Tokyo
)

2015-04-23 16:14:35 DEBUG - 終了

入力値99
2015-04-23 16:15:51 DEBUG - バリデーションチェック
2015-04-23 16:15:51 DEBUG - 99
2015-04-23 16:15:51 DEBUG - Array
(
    [0] => j
)

2015-04-23 16:15:51 DEBUG - DateTime Object
(
    [date] => 2015-07-08 16:15:51
    [timezone_type] => 3
    [timezone] => Asia/Tokyo
)

2015-04-23 16:15:51 DEBUG - 終了

入力値0
2015-04-23 16:16:40 DEBUG - バリデーションチェック
2015-04-23 16:16:40 DEBUG - 0
2015-04-23 16:16:40 DEBUG - Array
(
    [0] => j
)

2015-04-23 16:16:40 DEBUG - DateTime Object
(
    [date] => 2015-03-31 16:16:40
    [timezone_type] => 3
    [timezone] => Asia/Tokyo
)

2015-04-23 16:16:40 DEBUG - 終了
入力値100
2015-04-23 16:17:24 DEBUG - バリデーションチェック
2015-04-23 16:17:24 DEBUG - 100
2015-04-23 16:17:24 DEBUG - Array
(
    [0] => j
)

2015-04-23 16:17:24 DEBUG -
2015-04-23 16:17:24 DEBUG - 終了

結果

入力 出力日付 判定
0 先月最終日 true
1 今月1日 true
99 今月から99日後の正常に桁(月)上りした日付 true
100 false false

ちなみに-1なども失敗。
月のフォーマットmでも同様の結果になるが、
User Contributed Notesで同じようなことが書いてあった
マイナス評価なので、こちら側が悪い気がするが……

Laravel5のバリデーションの実装を見る

さすがにLaravel3時代の実装から改善されているかつ、流用できそうという期待からLaravel5の実装を見る。

Laravel5のValidator.php

Validatorクラス
	protected function validateDateFormat($attribute, $value, $parameters)
	{
		$this->requireParameterCount(1, $parameters, 'date_format');
		$parsed = date_parse_from_format($parameters[0], $value);
		return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
	}

多分これ。
$this->requireParameterCount()は要素数のバリデーションで無関係として、使用する関数が変わっている。
PHP: date_parse_from_format - Manual

パラメータ ¶
format
DateTime::createFromFormat() が理解できるフォーマット。

うーん嫌な予感。

動かしてみた結果

恐らくパラメーターの取り方は同じと信じてLaravel3に突っ込む。

        protected function validate_date_format($attribute, $value, $parameters)
        {
                \Log::debug("バリデーションチェック", true);
                \Log::debug($value, true);
                \Log::debug($parameters, true);
                \Log::debug(date_parse_from_format($parameters[0],$value), true);
                \Log::debug('終了', true);
                
                return date_create_from_format($parameters[0], $value) !== false;
        }

ざっくりdate_parse_from_format()の出力だけ。
読み飛ばしてOKです。

入力0
2015-04-23 16:55:26 DEBUG - Array
(
    [year] =>
    [month] =>
    [day] => 0
    [hour] =>
    [minute] =>
    [second] =>
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

入力1
2015-04-23 16:55:45 DEBUG - Array
(
    [year] =>
    [month] =>
    [day] => 1
    [hour] =>
    [minute] =>
    [second] =>
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

入力99
2015-04-23 16:56:04 DEBUG - Array
(
    [year] =>
    [month] =>
    [day] => 99
    [hour] =>
    [minute] =>
    [second] =>
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

入力100
2015-04-23 16:56:20 DEBUG - Array
(
    [year] =>
    [month] =>
    [day] => 10
    [hour] =>
    [minute] =>
    [second] =>
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 1
    [errors] => Array
        (
            [2] => Trailing data
        )

    [is_localtime] =>
)

errorもwarningも出ないので、Laravel5でも0~99はOKかもしれません。
これはもう、当たり前の仕様なのかもしれませんね。
確かに日付の繰越は便利なときが多々ありますし、日付情報を取り出す関数をバリデーションに使うことが間違いなのでしょうか。

betweenルールでバリデーションする

date_formatでは判定できそうに無いので、between:1,31で0や99の入力を弾きます。
(2月や30日までの場合はどうすんだって感じですが…あくまで日付のバリデートなので)
…が、うまくいかない。
0でも99でも100でもエラーメッセージが出ません。

betweenルールの実装を見る

Laravel3のValidator.php

Validatorクラス
	protected function validate_between($attribute, $value, $parameters)
	{
		$size = $this->size($attribute, $value);
		return $size >= $parameters[0] and $size <= $parameters[1];
	}

return前の$sizeを確認すると、1や2になっている。
$this->size()が余計な変換をしているはず。

sizeメソッドの実装を見る

	protected function size($attribute, $value)
	{
	 	// This method will determine if the attribute is a number, string, or file and
	 	// return the proper size accordingly. If it is a number, the number itself is
	 	// the size; if it is a file, the kilobytes is the size; if it is a
	 	// string, the length is the size.
		if (is_numeric($value) and $this->has_rule($attribute, $this->numeric_rules))
		{
			return $this->attributes[$attribute];
		}
		elseif (array_key_exists($attribute, Input::file()))
		{
			return $value['size'] / 1024;
		}
		else
		{
			return Str::length(trim($value));
		}
	}

numeric_ruleが設定していなければsize()は文字列の長さを返す模様。
なので、1は文字列長1、99は文字列長2でバリデーション成功、と。
マニュアルにも書いていました。
Laravel3

サイズ
与えられた文字数であること、もしくは数字項目の場合はその値であることをバリデートする属性です。

Laravel5

between:最小値,最大値
フィールドが指定された最小値と最大値の間のサイズであることをバリデートします。sizeルールと同様の判定方法で、文字列、数値、ファイルは評価されます。

size:値
フィールドは指定された値と同じサイズであることをバリデートします。文字列の場合、値は文字長です。数値項目の場合、値は整数値です。ファイルの場合、値はキロバイトのサイズです。

引用時に気づきましたがLaravel5(4にも?)にはこんなルールもあるんですね。こっちを使えば意図通りでしょうか。3にはナイヨー

digits_between:最小値,最大値
フィールドが整数で、桁数が最小値から最大値の間であることをバリデートします。

numeric_rules

protected $numeric_rules = array('numeric', 'integer');

integerルールを追加してバリデーションする

ここまでのルールはこんな感じ
'required|date_format:j|integer|between:1,31'
で、テストしようと数値を入力し、POSTした瞬間に気づく。
*「入力値なんだからstringじゃん。int型じゃないからintegerルールで弾かれるじゃん」*と。
しかし結果は成功。ちゃんと1~31までの入力に限定できました。

integerって整数値を判定しないで何を判定しているんだ。
と、なぜか成功したのに調べるハメに。

integerルールの実装を見る

Laravel3のValidator.php

Validatorクラス
	protected function validate_integer($attribute, $value)
	{
		return filter_var($value, FILTER_VALIDATE_INT) !== false;
	}

PHP: filter_var - Manual

filter_var — 指定したフィルタでデータをフィルタリングする

PHP: 検証フィルタ - Manual

ID 名前 オプション フラグ 説明
FILTER_VALIDATE_INT "int" default, min_range, max_range FILTER_FLAG_ALLOW_OCTAL, FILTER_FLAG_ALLOW_HEX  値が整数であるかどうか、オプションで指定した範囲内にあるかどうかを検証します。

filter_var($value, FILTER_VALIDATE_INT)の動作

ちょうど最近、投稿がありましたので引用させてもらいます。
php で 整数かチェックする(数値型文字列含む)

echo is_decimal(10); //true
echo is_decimal(10.01); //false
echo is_decimal("10"); //true
echo is_decimal("10.01"); //false
echo is_decimal("ABC"); //false
echo is_decimal(0x10); //true
echo is_decimal("0x10"); //false

function is_decimal($value) {
return filter_var($value, FILTER_VALIDATE_INT) !== false;
}


以上より、`integer`ルールは、引用元の題そのまま、整数(整数型文字列を含む)かチェックするルール、ということですね。
決して整数型であることの判定ではないですね。


#### `integer`ルールの欠点
`filter_var($value, FILTER_VALIDATE_INT) `が整数型文字列でもいいということは、入力値をキャストする必要がないので、ユーザー入力値のバリデーションに限れば歓迎することでした。
そして`between`ルールが有効になったことにより、`date_format`ルールも不要になりました。が!
別記事にはこんな事も……
["数字だけ"を判定する時に便利な関数](http://qiita.com/alphabet_h/items/d02edbfc2cc24cce1200)
>じゃあこっちのがいいじゃん!と思われるかもしれませんが、ちょっとまった!
実はこれ、'0123'という文字列を渡すと、falseを返してしまいます。
これは、0から始まる数字は10進数の整数として正しい形ではないと判断される為です。
オプションで指定できるFILTER_FLAG_ALLOW_OCTALフラグを用いて8進数表現(8進数の場合0から始まる必要がある)を許可することも可能ですが、「ゼロ (0) で始まる入力を八進数とみなします。 ゼロの後には 0-7 しか続けることができません。」と説明にもある通り'0912'のような数字はNGとなってしまいます。

>今回の条件は「変数が数字だけであるかどうか」なので、これらをfalseと返してしまうのは問題です。
また、"+123"という文字列を渡すと、trueを返してしまうので、コレも問題ですね。

ということで、`09`日なんて入力は弾かれてしまいます。
でも日付でこんな入力することって、無いこともないですよね。
`date_format`の時は受け入れていたので、`integer`を使わない方法を探します。

## `numeric`ルールを追加してバリデーションする
`between`ルールが入力を数値と判定するには、`integer`ルールの他にも`numeric`ルールを設定していればOKでした。

### `numeric`ルールの実装を見る
[Laravel3のValidator.php](https://github.com/laravel/laravel/blob/v3.2.14/laravel/validator.php)

```php:Validatorクラス
	protected function validate_numeric($attribute, $value)
	{
		return is_numeric($value);
	}

PHP: is_numeric - Manual

is_numeric — 変数が数字または数値形式の文字列であるかを調べる

これもキャスト無しで使えそうです。

指定した変数が数値であるかどうかを調べます。数値形式の文字列は以下の要素から なります。(オプションの)符号、任意の数の数字、(オプションの)小数部、 そして(オプションの)指数部。つまり、+0123.45e6 は数値として有効な値です。十六進表記(0xf4c3b00c など) や二進表記 (0b10100111001 など)、そして八進表記 (0777 など) も認められますが、この場合は符号や小数部、指数部を含めることはできません。

01を許したら+1もゆ、許された(と思ってましたがfilter_varでも+1はtrueでした)
整数以外の少数付きもtrueですし、惜しいですね。

帰ってきたdate_format

最終的には

  • 0や32~99は許すけど+1とかは許さないdate_formatルール
  • 0や32~は許さないが+1とかは許すnumeric|between:1,31ルール
    を組み合わせることで最強に見えました。

ctype_digit()のススメ

最後に今後の展望的なものを

今回の問題点は既存の数値判定に厳密な整数を判定するものが無いことでした。
そこで、
"数字だけ"を判定する時に便利な関数
でも紹介されているように、ctype_digit()関数を使えば、整数型文字列を正しく判定できるのではないかと

echo ctype_digit('+1'); // false
echo ctype_digit('1');  // true
echo ctype_digit('01'); // true
echo ctype_digit('1.1'); // false

これならばbetweenのみで入力値の制限が想定通りに出来そうです。
01を不正とする場合は………

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?