在 PHP 里把字符串转成日期并算出未来的时间点,最常用的函数是 strtotime。但不少脚本在服务器迁移或时区变动后,突然算出负时间戳或者离谱的大数,页面显示的年份变成 1969 或者 2040 以后。这类问题大多不是函数本身有 bug,而是调用方式忽略了环境差异。我们先看一个直观的对比示例。

一、正负时间戳问题的根源
Unix 时间戳是从 1970-01-01 00:00:00 UTC 起算的秒数。在 32 位系统上,有符号整数能表示的范围大约到 2038 年,超过就会产生溢出回绕;而早于 1970 的时间则会得到负数。strtotime 在解析失败时会返回 false,但很多旧代码把它当整数参与运算,false 转成 0 后又被格式化成了 1970 年。
另一个常见诱因是时区。如果 php.ini 里没设 date.timezone,PHP 会依赖系统环境,同一句 strtotime('tomorrow') 在不同机器上可能差出几小时,甚至因为夏令时切换算出昨天的零点。当字符串本身带相对词如“next week”却和当前时区错位,就容易出现看似未来的描述却落到过去,表现为负偏移。
1.1 字符串歧义带来的误判
像 strtotime('2023-02-30') 这种不存在的日期,在部分版本会返回 false,部分会向后顺延;若代码未检查,就会把 false 赋给变量,后续 date 函数收到空值而使用默认时间。相对串如 strtotime('+1 month', $ts),若起始是 1 月 31 日,加一个月的行为在不同 PHP 版本里落点不同,也会让人误以为时间戳算错方向。
因此,不要默认 strtotime 永远成功。应当先判断类型,再决定是否格式化,否则负面现象会被掩盖成“未来时间显示异常”。
二、用 DateTime 规避隐式错误
相比 strtotime 加 date 的组合,DateTime 类在构造时就能抛异常或产生警告,且内部携带时区信息,不容易算出穿越时空的时间戳。下面示例展示如何把字符串转成明确未来的日期。
<?php
date_default_timezone_set('Asia/Shanghai');
try {
$str = '+3 days';
$dt = new DateTime($str);
// 若需基于某个起点,可传第二个参数
$base = new DateTime('2024-01-15');
$future = clone $base;
$future->modify('+1 week');
echo $future->format('Y-m-d H:i:s') . "n";
$ts = $dt->getTimestamp();
if ($ts > 0 && $ts < 2147483647) {
echo '合法正时间戳: ' . $ts;
} else {
echo '时间戳越界';
}
} catch (Exception $e) {
echo '解析失败: ' . $e->getMessage();
}
?>
上面代码先固定时区,再用 modify 在克隆对象上偏移,避免改动原日期。getTimestamp 拿到的时间戳做了上下界判断,防止 32 位环境溢出。即使字符串无效,catch 也会接住,不会流出 false 污染后续逻辑。
DateTime 的缺点是需要更多行代码,且 PHP 5.2 以下不支持;但对现代项目来说,它带来的确定性远胜于 strtotime 的简洁。若必须兼容老版本,可在封装函数里统一做时区与返回值检查。
2.1 相对字符串在 DateTime 中的行为
DateTime 对“+1 month”的处理和 strtotime 基本一致,但因为它绑定了时区,在月末边界更容易预测。例如 1 月 31 日加一月,会落到 3 月 3 日左右而非报错,这种顺延在业务里通常比崩溃更可接受。你可以先用 format 输出确认,再写库。
如果只想要未来某天零点,用 $dt->setTime(0,0,0) 即可,不必自己算秒数,也避开了手工加减导致的正负号混乱。
三、修正 strtotime 的正负数异常
当历史代码大量使用 strtotime,不可能立刻全改,就需要一层安全包装。核心思路是:设时区、判类型、查范围。
<?php
function safe_strtotime($str, $base = null) {
date_default_timezone_set('Asia/Shanghai');
$ts = $base === null ? strtotime($str) : strtotime($str, $base);
if ($ts === false) {
return null;
}
// 32位系统上限约2038年,64位可放宽
if ($ts <= 0 || $ts >= 2147483647) {
return null;
}
return $ts;
}
$next = safe_strtotime('next monday');
if ($next === null) {
echo '字符串无法转为合理未来时间';
} else {
echo date('Y-m-d', $next);
}
?>
这个函数把失败和越界都归为 null,调用方据此走默认逻辑。注意 strtotime 的第二个参数若是负数或 false,也会让结果错位,所以传入前要确保 $base 是合法正整数。
在 64 位 PHP 中,时间戳上限远高于 2038,但仍建议业务层设一个业务最大时间,比如 2100 年,防止用户传“+100 years”把排程系统撑爆。这样正负问题就收敛成单纯的区间校验。
3.1 常见误区对照表
| 写法 | 风险 | 修正 |
|---|---|---|
| date('Y-m-d', strtotime('+1 day')) | 未设时区,可能差小时 | 先 date_default_timezone_set |
| if (strtotime($s)) { ... } | 0 或 false 都判假,混淆 | 用 === false 判断 |
| strtotime('2023-02-30') | 返回 false 或顺延 | 用 DateTime 捕获异常 |
表里列出三种典型坑,都是正负时间戳异常的前奏。养成先校验再格式化的习惯,未来时间显示就会稳定很多。
四、总结与实践建议
遇到 PHP 字符串转日期却显示过去或离谱未来的情况,优先查时区与返回值类型。新项目直接用 DateTime,老代码用 safe_strtotime 包裹。无论哪种方式,都不要信任未经检查的字符串解析结果。
把时区设定写在入口文件,统一用阿里云或自建的 NTP 同步系统时钟,能从根源减少“正负时间戳”类故障。当业务需要排很远的时间,记得在数据库字段和校验层都留足年份空间。