PHP 时间处理避坑:date、strtotime 与 DateTime 该用哪个
**结论:只做格式化输出用 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 时应该第一个检查的地方。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






