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

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

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

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

分解すると

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

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

# バリデーションの実装を見る
実装は`Laravel\Validator`クラスの`validate_date_format()`
[Laravel3のValidator.php](https://github.com/laravel/laravel/blob/v3.2.14/laravel/validator.php)

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

`date_create_from_format`は`DateTime::createFromFormat`のエイリアスである。
[PHP: date_create_from_format - Manual](http://php.net/manual/ja/function.date-create-from-format.php)
>この関数は次の関数のエイリアスです。 [DateTime::createFromFormat()](http://php.net/manual/ja/datetime.createfromformat.php)

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

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

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

## 動かしてみる

```php:デバッグコード入り
        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;
        }
```

```bash:入力値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 - 終了

```

```bash:入力値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 - 終了

```

```bash:入力値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 - 終了
```

```bash:入力値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で同じようなことが書いてあった](http://php.net/manual/ja/datetime.createfromformat.php#115270)
マイナス評価なので、こちら側が悪い気がするが……

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

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

[Laravel5のValidator.php](https://github.com/laravel/framework/blob/5.0/src/Illuminate/Validation/Validator.php)

```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](http://php.net/manual/ja/function.date-parse-from-format.php)

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

うーん嫌な予感。

## 動かしてみた結果
恐らくパラメーターの取り方は同じと信じてLaravel3に突っ込む。

```php
        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です。

```bash:入力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] =>
)

```

```bash:入力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] =>
)

```


```bash:入力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] =>
)

```


```bash:入力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](https://github.com/laravel/laravel/blob/v3.2.14/laravel/validator.php)

```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`メソッドの実装を見る

```php
	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](http://laravel3.kore1server.com/docs/validation#rule-size)
>サイズ
与えられた文字数であること、もしくは数字項目の場合はその値であることをバリデートする属性です。

[Laravel5](http://readouble.com/laravel/5/0/0/ja/validation.html#rule-between)
>between:最小値,最大値
フィールドが指定された最小値と最大値の間のサイズであることをバリデートします。sizeルールと同様の判定方法で、文字列、数値、ファイルは評価されます。

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

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

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

`numeric_rules`は

```php
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](https://github.com/laravel/laravel/blob/v3.2.14/laravel/validator.php)

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

[PHP: filter_var - Manual](http://php.net/manual/ja/function.filter-var.php)
>filter_var — 指定したフィルタでデータをフィルタリングする

[PHP: 検証フィルタ - Manual](http://php.net/manual/ja/filter.filters.validate.php)


>
| 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 で 整数かチェックする（数値型文字列含む）](http://qiita.com/imamura0825/items/60cc09bab1f3726fc669)

>```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](http://php.net/manual/ja/function.is-numeric.php)

>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()`のススメ
最後に今後の展望的なものを

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

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

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