以下も併せてお読みください。
導入
上に述べた記事でさまざまな入力パターンについて検証していますが、 **「どこまでが有効と見なされてパースされるか、どこからが無効と見なされて例外がスローされるか」**が非常に曖昧でありました。
A. これは通る
echo (new \DateTime('2016/02/29'))->format('Y-m-d'); // 2016-02-29
B. これも通る
echo (new \DateTime('2015/02/29'))->format('Y-m-d'); // 2015-03-01
C. これは通らない
echo (new \DateTime('2015/01/32'))->format('Y-m-d'); // Fatal error: Uncaught exception 'Exception' with message 'DateTime::__construct(): Failed to parse time string (2015/02/32) at position 9 (2): Unexpected character'
ここではA
だけを通過させる方法を考えてみたいと思います。
メソッドとその使用例
DateTime::getLastErrorsまたはDateTimeImmutable::getLastErrorsを用いると、上記では通過していたものについてのエラーも調べることが出来ます。結果は以下の形式の連想配列として得られます。
キー | 値 | ||||
---|---|---|---|---|---|
warning_count |
warning の同時発生件数 |
||||
warnings |
warning に関する連想配列
|
||||
error_count |
error の同時発生件数 |
||||
errors |
error に関する連想配列
|
Aについての返り値
new \DateTime('2016/02/29');
var_dump(\DateTimeImmutable::getLastErrors());
/*
array(4) {
["warning_count"]=>
int(0)
["warnings"]=>
array(0) {
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}
}
*/
Bについての返り値
new \DateTime('2015/02/29');
var_dump(\DateTimeImmutable::getLastErrors());
/*
array(4) {
["warning_count"]=>
int(1)
["warnings"]=>
array(1) {
[11]=>
string(27) "The parsed date was invalid"
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}
}
*/
Cについての返り値
try {
new \DateTime('2015/01/32');
} catch (\Exception $e) {
var_dump(\DateTimeImmutable::getLastErrors());
}
/*
array(4) {
["warning_count"]=>
int(0)
["warnings"]=>
array(0) {
}
["error_count"]=>
int(1)
["errors"]=>
array(1) {
[9]=>
string(20) "Unexpected character"
}
}
*/
注意点
エラーレベルには2種類がある
エラーレベル | 発生時の動作 | 対応する導入の例 |
---|---|---|
error |
Exception をスローし、且つエラー取得メソッドで報告する |
C |
warning |
Exception をスローせず、エラー取得メソッドで報告する |
B |
対象メソッドを1回コールするごとにリセットされる
直近のコールでエラーが発生しなかった場合もクリアされる
new \DateTime('2015/02/29');
new \DateTime('2016/02/29');
var_dump(\DateTime::getLastErrors());
/*
array(4) {
["warning_count"]=>
int(0)
["warnings"]=>
array(0) {
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}
}
*/
DateTime::getLastErrorsと
DateTimeImmutable::getLastErrorsは同一視される
2つのメソッドはクラスごとに区別されません。
区別せずに取得してしまう例
new \DateTimeImmutable('2015/02/29');
var_dump(\DateTime::getLastErrors());
/*
array(4) {
["warning_count"]=>
int(1)
["warnings"]=>
array(1) {
[11]=>
string(27) "The parsed date was invalid"
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}
}
*/
全てのメソッドが対象になるわけではない
文字列からパースする以下のメソッドに限られます。
- DateTime::__construct
- DateTime::modify
- DateTime::createFromFormat
- DateTimeImmutable::__construct
- DateTimeImmutable::modify
- DateTimeImmutable::createFromFormat
スルーされる例
$date = (new \DateTime)->setDate(2015, 1, 32);
echo $date->format('Y-m-d') . PHP_EOL;
var_dump(\DateTime::getLastErrors());
/*
2015-02-01
array(4) {
["warning_count"]=>
int(0)
["warnings"]=>
array(0) {
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}
}
*/
エラー取得メソッドの利用例
バリデーションに利用
正規表現を書く必要が無くなるのは大きなメリットかもしれません。
実装例
function check_datetime_format($format, $time) {
\DateTime::createFromFormat($format, $time);
$info = \DateTime::getLastErrors();
return !$info['errors'] && !$info['warnings'];
}
テスト (何も表示されなければ成功)
assert('check_datetime_format("Y/m/d", "2015/01/01") === true');
assert('check_datetime_format("Y/m/d", "15/1/1") === true'); # 0015/01/01
assert('check_datetime_format("y/m/d", "15/1/1") === true'); # 2015/01/01
assert('check_datetime_format("Y/m/d", "2015/001/001") === false');
assert('check_datetime_format("Y/m/d", "2015/01/31") === true');
assert('check_datetime_format("Y/m/d", "2015/01/32") === false');
assert('check_datetime_format("Y/m/d", "2015/02/29") === false');
assert('check_datetime_format("Y/m/d", "2016/02/29") === true');
assert('check_datetime_format("H:i:s", "02:03:04") === true');
assert('check_datetime_format("H:i:s", "2:03:04") === true');
assert('check_datetime_format("H:i:s", "02:3:04") === false');
assert('check_datetime_format("H:i:s", "02:03:4") === false');
但し、細かい挙動のカスタマイズは出来ず、あくまでPHPの判断基準に従うことになります。オリジナルの仕様が明確に定められている場合は正規表現を使うべきでしょう。
継承したクラスでオーバーライドして利用
ここでは、全ての対象メソッドで厳密なチェックを行うようにオーバーライドしてみます。
- 複数のエラーが発生していた場合は、先頭の1つだけを
Exception
のメッセージとします。 - メッセージ内容に一貫性を持たせるように、標準の仕様に従ったメッセージ形式にしています。
-
DateTime::createFromFormatあるいはDateTimeImmutable::createFromFormatでも
Exception
をスローするようにしています。これは標準の仕様とは異なります。
2クラスともに同じコードを書くのは冗長なのでトレイトの利用を考えましたが、コンストラクタだけはトレイト中に含められないようなので、今回はべた書きしました。
実装例
class StrictDateTime extends \DateTime {
protected function checkErrors($method, $time) {
static $formats = [
'error' => '%s(): Failed to parse string (%s) at position %d (%s): %s',
'warning' => '%s(): Failed to parse string (%s): %s',
];
$info = \DateTime::getLastErrors();
foreach ($info['errors'] as $offset => $error) {
throw new \Exception(sprintf($formats['error'], $method, $time, $offset, $time[$offset], $error));
}
foreach ($info['warnings'] as $offset => $warning) {
throw new \Exception(sprintf($formats['warning'], $method, $time, $warning));
}
}
public function __construct($time = 'now', \DateTimeZone $timezone = null) {
parent::__construct($time, $timezone);
self::checkErrors(__METHOD__, $time);
}
public function modify($modify) {
parent::modify($modify);
self::checkErrors(__METHOD__, $modify);
return $this;
}
public static function createFromFormat($format, $time, \DateTimeZone $timezone = null) {
if ($timezone !== null) {
$return = parent::createFromFormat($format, $time, $timezone);
} else {
$return = parent::createFromFormat($format, $time);
}
self::checkErrors(__METHOD__, $time);
return $return;
}
}
class StrictDateTimeImmutable extends \DateTimeImmutable {
protected function checkErrors($method, $time) {
static $formats = [
'error' => '%s(): Failed to parse string (%s) at position %d (%s): %s',
'warning' => '%s(): Failed to parse string (%s): %s',
];
$info = \DateTimeImmutable::getLastErrors();
foreach ($info['errors'] as $offset => $error) {
throw new \Exception(sprintf($formats['error'], $method, $time, $offset, $time[$offset], $error));
}
foreach ($info['warnings'] as $offset => $warning) {
throw new \Exception(sprintf($formats['warning'], $method, $time, $warning));
}
}
public function __construct($time = 'now', \DateTimeZone $timezone = null) {
parent::__construct($time, $timezone);
self::checkErrors(__METHOD__, $time);
}
public function modify($modify) {
$return = parent::modify($modify);
self::checkErrors(__METHOD__, $modify);
return $return;
}
public static function createFromFormat($format, $time, \DateTimeZone $timezone = null) {
if ($timezone !== null) {
$return = parent::createFromFormat($format, $time, $timezone);
} else {
$return = parent::createFromFormat($format, $time);
}
self::checkErrors(__METHOD__, $time);
return $return;
}
}
テスト
誰か書いてください