【PHP8.3】DateTimeのエラーハンドリングがいいかんじになるよ

みなさん映画スーパーマリオ見ましたか？
私はIMAX吹替で見てきたのですが……いやー…………めっちゃ面白かったですね！！
特にラストシーンでクッパが親指を立てながら溶岩湖に沈んでいくシーンは涙無しには見られませんでした。


さて全く関係ないのですがDateTimeの話です。
[DateTimeImmutable::__construct](https://www.php.net/manual/ja/datetimeimmutable.construct.php)に[不正な値を突っ込んだ](https://3v4l.org/u3gtT)らどうなるでしょう。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/4658e793-0b82-576e-19b2-82d1558b7863.png)

:::note
エラーがあった場合は Exception を発生させます。
:::

わおアバウト。

ということでPHP8.3からは、もっと細かくいいかんじに例外を扱えるようになります。
以下は該当のRFC、[More Appropriate Date/Time Exceptions](https://wiki.php.net/rfc/datetime-exceptions)の紹介です。


# PHP RFC: More Appropriate Date/Time Exceptions

## Introduction

このRFCでは、Date/Timeエクステンションに固有の例外とエラーを導入します。

現在、Date/Timeが発生させるのはただの`Exception`と`Error`です。
これは全く具体的ではないため、Date/Timeの例外を適切に捕捉することができません。

なお本RFCの範囲外ですが、ついでに警告・エラーのメッセージを改善します。


## Proposal

本RFCは、警告やエラーを整理し、Date/Timeエクステンション固有の例外やエラーを導入します。
概要は以下のとおりです。

readonlyプロパティの変更など汎用的な例外についてはErrorをスローします。
これはユーザランドコードのミスに対してPHP本体が行う処理です。

ユーザランドコードや自由入力テキストからの不正な入力についてはValueErrorを発生します。

不正なTimezoneなど、無効なデータはDateErrorを発生します。

初期化されていないオブジェクトにはDateObjectErrorを発生します。

ユーザ入力などテキストからのパースに失敗すると、DateExceptionを継承した例外を発生します。
・タイムゾーン生成のエラーはDateInvalidTimeZoneExceptionを発生します。
・`DateTime::sub()`に相対時刻を渡すなど、実行できない操作などはDateInvalidOperationExceptionを発生します。
・パースに失敗して`DateTime`オブジェクトが生成できないときはDateMalformedStringExceptionを発生します。
・パースに失敗して`DatePeriod`オブジェクトが生成できないときはDateMalformedPeriodStringExceptionを発生します。

なお、Date/Timeを手続き型として使う場合は影響を受けず、現在と同じ警告・エラーが発生します。


## Backward Incompatible Changes

互換性のない変更点。

エポックがPHPのintに収まらない場合、ValueErrorではなくDateRangeErrorを発生させるようになりました。
DateRangeErrorはValueErrorのサブクラスではありません。
これは32ビットプラットフォームでのみ問題になります。

これまで`DateTime::sub()`で発生していた、`Only non-special relative time specifications are supported for subtraction`の警告はDateInvalidOperationExceptionの例外になります。
これを警告で放置するのは、有用なふるまいではありません。

これまで`DateInterval`で発生していた`Unknown or bad format (%s) at position %d (%c): %s`および`String '%s' contains non-relative elements`の警告はDateMalformedIntervalStringExceptionの例外になります。

## Proposed PHP Version(s)

PHP8.3

## RFC Impact

エクステンションやopcodeに対する影響はありません。

## Unaffected PHP Functionality

手続き型での使用については、これまでと動作は変わりません。
オブジェクト型で使用する場合のみ影響を受けます。

## Voting

投票者の2/3の賛成で受理されます。

投票期間は2022/12/15から2022/12/31です。
本RFCは、賛成25反対1の賛成多数で可決されました。


## Implementation

https://github.com/php/php-src/commit/66a1a911f1d6cd4b89c0bb5577fa77f1d6a2cb96


# 感想

手続き型とオブジェクト型で処理分ける必要ある？

```php
$i = DateInterval::createFromDateString('last Monday of');

$sub = (new DateTimeImmutable())->sub($i);

// PHP8.2  Warning: DateTimeImmutable::sub(): Only non-special relative time specifications are supported for subtraction
// PHP8.3  Fatal error: DateMalformedIntervalStringException 


$sub = date_sub(new DateTime(), $i);

// PHP8.2  Warning: date_sub(): Only non-special relative time specifications are supported for subtraction
// PHP8.3  Warning: date_sub(): Only non-special relative time specifications are supported for subtraction
```

全部Exceptionにしてしまっていいと思うのだが。

どういう経緯があったのかは調べていないのでわかりませんが、まあ今どき`date_xxx`を使ってるコードなんて古すぎてどうしようもないだろうからそのままにしといてやろう的な判断でもあったのかもしれませんね。

ということでDateTimeを使っている場合、今後はよりいいかんじに例外を扱えるようになります。


あとどうでもいいけど調査中、[date_sub](https://www.php.net/manual/ja/function.date-sub.php)の第一引数が[DateTimeInterface](https://www.php.net/manual/ja/class.datetimeinterface.php)ではなく[DateTime](https://www.php.net/manual/ja/class.datetime.php)だということに気が付いた。

```php
$sub = date_sub(new DateTimeImmutable(), new DateInterval('P1D'));
  // Fatal error: Uncaught TypeError: date_sub(): Argument #1 ($object) must be of type DateTime, DateTimeImmutable given
```

どうして？