PHP 时间处理避坑:date、strtotime 与 DateTime 该用哪个

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-14 22:12 ·11 浏览 ·0 回复

**结论:只做格式化输出用 `date()`,解析一次性可控字符串可以用 `strtotime()`,但只要涉及时区、时间加减、差值计算、跨月跨年、存库落地,一律用 `DateTimeImmutable`。** 记住这条,能避开 PHP 时间处理里九成以上的坑。下面按「先立两条铁律,再逐个说坑」的顺序讲清楚。

铁律一:存库统一 UTC,展示时才转时区

时间问题本质是时区问题,不是函数问题。Unix 时间戳(从 1970-01-01 00:00:00 UTC 起的秒数)本身不带时区,`time()` 返回的值全世界一致;而 `date()`、`strtotime()` 的解析结果都会受 `date_default_timezone_set()` 影响。

所以标准做法是:数据库存 UTC 时间戳(`INT`)或 UTC 的 `DATETIME`,展示时再用用户时区转换。很多「显示时间差 8 小时」的排查到最后都是:php.ini 里没写 `date.timezone`,PHP 退到 UTC,而业务代码按北京时间理解,两边对不上。

顺便提醒:MySQL 的 `DATETIME` 不带时区语义,`TIMESTAMP` 会跟着连接时区自动转换,两者混用是排查噩梦,团队里统一一种。

铁律二:别用比较字符串的方式比较时间

`'2024-1-5' > '2024-01-04'` 这种写法在格式不统一时会给出错误答案。时间比较只认两种形式:整数时间戳,或者 `DateTimeInterface` 对象(PHP 原生支持 `<`、`>`、`==` 按时刻比较)。

date():它只会「翻译」,不会计算

`date()` 的职责单一——把一个时间戳翻译成人类可读字符串,第二个参数默认 `time()`。它不会算时间差,也不会「加一个月」。

结论:`date()` 只适合「我手上已经有一个确定的时间戳,现在要打印出来」这一种场景。

常见错误写法是 `date('Y-m-d', strtotime('+1 month'))` 这种嵌套,一旦 `strtotime` 因输入异常返回 `false`,`date()` 会把 `false` 当 `0` 处理,直接输出 `1970-01-01`,而且不报错——这是线上最隐蔽的一类 bug。

strtotime():快,但必须处理两个坑

第一个坑:解析失败返回 `false`。所以判断必须写全 `=== false`,不能写 `if (!$ts)`,因为合法的 `0` 也会被误判。

$ts = strtotime($input);
if ($ts === false) {
    // 输入不合法,走兜底逻辑
}

第二个坑:相对格式存在「溢出」行为。`strtotime('2024-01-31 +1 month')` 得到的是 3 月 2 日,不是 2 月 29 日——因为它是先给「月份数」加一(变成 2 月 31 日),再把不存在的日期往后溢出。正确表达「下个月的同一天」应该显式用修饰符,比如 `strtotime('first day of next month')`,或者用 `DateTimeImmutable` 再手工钳制日期。

另外,PHP 8.1 起给 `date()`、`strtotime()` 传 `null` 会触发弃用警告,Clara BBS 支持 PHP 7.4–8.5 的宽版本区间,插件代码里不要依赖隐式的 `null → 0` 转换。

DateTimeImmutable:默认就该选它

`DateTimeImmutable`(不可变日期时间对象)和 `DateTime` 的唯一区别是:前者的 `add()`、`modify()`、`setDate()` 全部返回新对象,不动原值;后者是原地修改。

结论:除非你明确需要原地修改,否则一律用 `DateTimeImmutable`,因为它从根上消灭了「对象被别处改掉」的幽灵 bug。

它的几个实用点:

  • 时区显式化:`new DateTimeImmutable('2024-01-01 00:00:00', new DateTimeZone('Asia/Shanghai'))`,之后 `setTimezone()` 随便转,不用碰全局默认值。
  • 差值计算:`$a->diff($b)` 返回 `DateInterval`,天数直接读 `$interval->days`,跨月跨年不用自己算。
  • 格式化:`->format('Y-m-d H:i:s')`,和 `date()` 同一套格式字符,迁移成本几乎为零。

一份决策清单

  • 已知时间戳 → 展示字符串:`date()`
  • 解析格式固定的用户输入,且输入完全可控:`strtotime()`,但必须判 `=== false`
  • 算「7 天后」「还剩几天到期」「本月第一天」:`DateTimeImmutable`
  • 涉及多时区、用户自定义时区:`DateTimeImmutable`
  • 要落库:先转 UTC 再存

写在 Clara BBS 插件开发场景里

Clara BBS 是无需 Composer、上传即用的轻量 PHP 论坛系统,插件放在 `content/plugins` 下走运行时钩子加载,这意味着你没法像常规项目那样 `composer require` 一个 Carbon 之类的日期库——把 `DateTimeImmutable` 用熟就是刚需。

具体到业务:Cron::register 注册的定时任务是懒触发、零配置的,而「连续签到天数」「会员到期自动失效」这类逻辑跟「自然日」强绑定,一旦服务器时区、PHP 时区、数据库时区三者不一致,就会整体差一天。稳妥做法是在插件里显式指定时区构造时间对象,不要依赖全局默认值。

收束

时间处理的答案其实很清晰:`date()` 管输出,`strtotime()` 管可控输入的快速解析(且必须判 `false`),其余一律交给 `DateTimeImmutable`,存储统一 UTC、展示时再转时区。真正让人踩坑的从来不是函数本身,而是三处时区配置不一致——那才是排查时间 bug 时应该第一个检查的地方。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-350.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~