PHP 8.6 的 Time\Duration:让「一段长度」和日历语义分开
PHP 8.6 新增了 Time\Duration,它是一个 final readonly 类,用来表示秒表式的、固定长度的时间,精度到纳秒。它提供按单位构造的工厂方法、四则运算方法,以及比较支持。
基本用法
use Time\Duration;
$oneSecond = Duration::fromSeconds(1);
$halfSecond = $oneSecond->divideBy(2);
$total = $oneSecond->add($halfSecond);
$delay = Duration::fromMilliseconds(100)->multiplyBy(2 ** $attempt);
$total > $delay; // 比较运算符可用
Duration 是不可变对象,可以直接用比较运算符比较大小。它也可以从 ISO 8601 duration 字符串构造。设计目标,是让它成为一个共享类型:原本核心函数和新的 polling API 只能接收散落的 int/float 参数,现在可以统一接收 Duration。
为什么还需要另一个时长类型
PHP 的 DateTime 扩展有一处短板:它提供了 DateInterval 用于按指定间隔推算新日期,但 DateInterval 只适用于日历日期,不适用于一般的时间值。
Time\Duration 表示的是流逝时长的固定值,比如一个超时时间,精度为纳秒。和 DateInterval 的关键差别在于:Duration 不包含「天」「月」这类日历单位,所以它的长度是无歧义的。
P1M 这类写法本身就是歧义来源:一个月是 28 天、29 天、30 天还是 31 天,取决于它落在哪个日历位置上。这种不确定性对于推算日期是必要的,但对于「等 200 毫秒后重试」这类场景则完全不合适。
类提供的能力
- 工厂方法:
Time\Duration::fromSeconds()、Time\Duration::fromMilliseconds()、Time\Duration::fromIso8601DurationString() - 运算:
Time\Duration::add()、Time\Duration::sub()、Time\Duration::multiplyBy()、Time\Duration::divideBy() - 符号变换:
Time\Duration::negate()、Time\Duration::absolute() - 排序回调:
Time\Duration::compare()
边界情况
当 Duration 超出可表示范围(约 292 年)时,会抛出 Time\TimeException。当传入的 ISO 8601 duration 字符串非法时,同样抛出 Time\TimeException。292 年这个上限来自纳秒精度下的整数位宽,因此它是硬性上限,而不是可配置项。
取舍:什么时候仍然使用 DateInterval
判断标准可以简化成一句话:这段长度是否依赖于日历。
- 重试退避、超时、限流窗口、缓存 TTL —— 这些都是纯流逝时间,用
Duration。没有「月」「日」单位,就不用解释一个月的含义。 - 「下个月同一天」「三个工作日之后」「每年提醒」 —— 这些仍然使用
DateInterval,因为答案确实取决于起始日期和时区规则。
把两者混在同一个类型里,代价就是任何一次「加一个月」的运算都要带着起始时间做上下文,而 Duration 通过砍掉日历单位把这件事排除掉了。
指数退避示例
把退避时间表示为 Duration 之后,第 $attempt 次重试的等待量直接写成乘法:
use Time\Duration;
$delay = Duration::fromMilliseconds(100)->multiplyBy(2 ** $attempt);
// $delay 是 Duration,可直接传给接受该类型的 API,
// 也可与其它 Duration 做比较,无需在调用点反复换算单位
基准值 100 ms 是 Duration,multiplyBy(2 ** $attempt) 返回新的 Duration,全程不出现裸数字,单位换算收敛在构造那一行。
版本状态
PHP 8.6 计划于 2026 年 11 月 19 日 GA。当前最新预发布版为 8.6.0RC7(2026 年 11 月 12 日)。本文涉及的行为描述基于该预发布阶段,尚未在生产环境实测;RC 到 GA 之间若 API 有调整,以最终发布说明为准。