编程

PHP 8.3: 细粒化 DateTime 异常

1517 2023-11-02 00:40:00

在 PHP 8.3 中,Date/Time 扩展引入了特定于扩展的颗粒度异常和错误类,以便更好地表达错误和异常的状态。这使得捕获日期相关的异常更为简单和干净。

在 PHP 8.3 之前,DateTime 扩展使用标准的 \Exception\Error

新增的 Exception/Error 类继承了现有的 \Error\Exception 类,这意味着现有捕获 \Exception 或者  \Error 异常的代码,应该可以继续捕获这些错误。

请注意,Date/Time 扩展继续抛出 \ValueError (当传入无效值到函数/方法中时)、\TypeError(标准类型错误) 及 \Error 异常(尝试修改只读属性,反系列化出错等)。

新增异常类

PHP 8.3 添加了九个 Exception/Error 类到 Date/Time 扩展中。用户空间 PHP 代码也可以抛出这些异常,虽然按照惯例,这些异常只未 DateTime 扩展保留。

Date/Time 异常类的结构

以下图表显示了新的 DateError

Throwable
  ├── Error
  |     └── DateError
  |             ├── DateObjectError
  │             └── DateRangeError
  └── Exception
        └── DateException
                ├── DateInvalidTimeZoneException
                ├── DateInvalidOperationException
                ├── DateMalformedStringException
                ├── DateMalformedIntervalStringException
                └── DateMalformedPeriodStringException

DateError 及它的子类 (DateObjectErrorDateRangeError)为  Date 扩展自身或者 PHP 运行时的错误保留的,与传递给 Date/Time 类或函数的实际值无关。

DateException 及其子类是在值本身无效或者畸形时抛出。对于接受用户提供或者动态日期时间值的 PHP 应用,DateException 是适合于捕捉的异常。

DateError

如果底层的 timelib 数据库崩溃,就会抛出 DateError 错误。这不是常见的例子,为的是说明与 PHP 安装本身相关的错误。

DateObjectError

当 Date/Time 类对象没有正确初始化时,会抛出 DateObjectError 错误。一个常见的例子是,当用户空间 PHP 类扩展 Date/Time 类,但没有在构造器中调用 parent::__construct() 初始化它。

class Foo extends DateInterval {
    public function __construct() {
        // Does not call parent::__construct();
    }
}

$interval = new Foo();
$interval->format('s');
DateObjectError: Object of type Foo (inheriting DateInterval) has not been correctly initialized by calling parent::__construct() in its constructor

此外,尝试比较未初始化的 Date/Time 对象也将导致 DateObjectError 错误:

DateObjectError: Trying to compare uninitialized DateTimeZone objects

DateRangeError

DateRangeError 错误将在尝试处理一个超过 PHP 整型值的日期时抛出。

DateRangeError: Epoch doesn't fit in a PHP integer

DateException

DateException 异常是 \Exception 子类的通用类型,会在用户提供无效值时抛出。

DateInvalidTimeZoneException

DateInvalidTimeZoneException 异常会在尝试使用不能识别的时区名实例化 DateTimeZone 类对象,或者尝试设置超出时区范围的阈值是抛出。

new DateTimeZone("DoesNotExists");
new DateTimeZone("-9999");
DateInvalidTimeZoneException: DateTimeZone::__construct(): Unknown or bad timezone (DoesNotExists)
DateInvalidTimeZoneException: DateTimeZone::__construct(): Timezone offset is out of range (-9999)

DateInvalidOperationException

DateInvalidOperationException 异常会在尝试对 DateTime 对象进行无效操作时抛出。当前,在特殊的时间规范上调用 DateTimeInterface::sub 会抛出该异常。PHP 8.3 之前,这将导致 PHP 警告。

$now = new DateTimeImmutable("1992-09-16 10:44:00 CET");
$e = DateInterval::createFromDateString('next wednesday');
$now->sub($e);
DateInvalidOperationException: DateTimeImmutable::sub(): Only non-special relative time specifications are supported for subtraction

DateMalformedStringException

DateMalformedStringException 异常会在 DateTime 扩展不能从给定的字符串中解析处有效的日期/时间时抛出。最常见的情况是,DateTime/DateTimeImmutable 对象使用无效的日期/时间字符串实例化时。 

new DateTimeImmutable('half-life 3 release date');
DateMalformedStringException: Failed to parse time string (half-life 3 release date) at position 0 (h): The timezone could not be found in the database

DateMalformedIntervalStringException

类似于 DateMalformedStringExceptionDateMalformedIntervalStringException 异常在 Date/Time 扩展类碰到无效时间间隔字符串时抛出。

new DateInterval('until tomorrow');
new DateInterval('1992-09-16T10:44:00Z');
DateInterval::createFromDateString('next wednesday 10:44');
DateMalformedIntervalStringException: Unknown or bad format (until tomorrow)
DateMalformedIntervalStringException: Failed to parse interval (1992-09-16T10:44:00Z)
Uncaught DateMalformedIntervalStringException: String 'next wednesday 10:44' contains non-relative elements

DateMalformedPeriodStringException

DateMalformedPeriodStringException 异常在尝试使用格式错误的字符串初始化 DatePeriod 类实例时抛出。

new DatePeriod('1 mississippi');
new DatePeriod('10D');
new DatePeriod("R4");
new DatePeriod("2012-07-01T00:00:00Z/P7D");
DateMalformedPeriodStringException: Unknown or bad format (1 mississippi)
DateMalformedPeriodStringException: Unknown or bad format (10D)
DateMalformedPeriodStringException: DatePeriod::__construct(): ISO interval must contain a start date, "R4" given
DateMalformedPeriodStringException: DatePeriod::__construct(): Recurrence count must be greater than 0

Exception 类 Polyfill

虽然不可能将新的颗粒度异常更改移植到老版本中,但可以将这些异常/错误类 polyfill 补丁到老版本中。如果在 PHP >8.3 环境中,应用需要在抛出异常之前反系列化异常对象,这可能会有所帮助。PHP 应用或库决定使用新的 Exception/Error 类时,这也很有用。

class DateError extends Error {}

class DateObjectError extends DateError {}
class DateRangeError extends DateError {}

class DateException extends Exception {}

class DateInvalidTimeZoneException extends DateException {}
class DateInvalidOperationException extends DateException {}
class DateMalformedStringException extends DateException {}
class DateMalformedIntervalStringException extends DateException {}
class DateMalformedPeriodStringException extends DateException {}

向后兼容性影响

从技术上讲,这是向后兼容性破坏,因为在 PHP < 8.2 中的特定警告会在 PHP 8.3 及以后的版本中变成异常。以下异常会替换 PHP 8.2 及此前的版本中的警告。

  • DateRangeError: Epoch doesn't fit in a PHP integer
  • DateInvalidOperationException: Only non-special relative time specifications are supported for subtraction
  • DateMalformedIntervalStringException: String '%s' contains non-relative elements