PHP 从 12 月 29 日起生成错误的 next payment date
支付老李
结论
不要直接以当前日期为基准计算 +2 month。先将日期固定为当月第一天,再增加相应的月份:
<?php
$today = (int) date('j');
$monthsToAdd = $today < 15 ? 1 : 2;
$nextPaymentDate = date(
'Y-m-d',
strtotime("first day of this month +{$monthsToAdd} months")
);
这样即使当前日期是 28、29、30 或 31 日,也能正确得到目标月份的第一天。
原因
strtotime('+2 month') 会保留当前日期的“日”。例如,以 2014-12-29 为基准计算时,PHP 会先尝试得到:
2015-02-29
由于 2015 年 2 月没有 29 日,这个日期会溢出,并被规范化为 3 月。此时再使用:
date('Y-m-01', ...)
只能将已经溢出到 3 月的日期格式化为当月第一天,因此结果是:
2015-03-01
date('Y-m-01') 中的 01 只控制最终的输出格式,无法阻止 strtotime() 在计算月份时发生日期溢出。
推荐写法:使用 DateTimeImmutable
支付日期属于业务数据,使用对象式日期 API 通常更清楚:
<?php
$now = new DateTimeImmutable('now');
$monthsToAdd = (int) $now->format('j') < 15 ? 1 : 2;
$nextPaymentDate = $now
->modify('first day of this month')
->modify("+{$monthsToAdd} months")
->format('Y-m-d');
echo $nextPaymentDate;
代码先执行:
->modify('first day of this month')
将日期固定到当月 1 日,然后再增加一个月或两个月。每个月都有 1 日,因此不会出现月底溢出。
如果系统使用固定的业务时区,建议明确指定:
<?php
$timezone = new DateTimeZone('Asia/Shanghai');
$now = new DateTimeImmutable('now', $timezone);
$monthsToAdd = (int) $now->format('j') < 15 ? 1 : 2;
$nextPaymentDate = $now
->modify('first day of this month')
->modify("+{$monthsToAdd} months")
->format('Y-m-d');
关于 15 日的边界
题目原来的判断是:
$today >= $checkDate
对应的规则是:
- 1~14 日购买:下个月 1 日付款
- 15 日及以后购买:下下个月 1 日付款
这符合“15 日之前”的字面含义,因为 15 日当天不算在 15 日之前。如果业务要求 15 日当天仍按下个月处理,则应改为:
$monthsToAdd = (int) $now->format('j') <= 15 ? 1 : 2;
不建议的写法
不要连续调用两次 strtotime('next month'):
$nextPaymentDate = date('Y-m-01', strtotime('next month'));
$nextPaymentDate = date($nextPaymentDate, strtotime('next month'));
第二次调用并不是以第一次生成的日期为计算基准。另外,传给 date() 的第一个参数会被解释为格式字符串,而不是基准日期。
如果需要从某个日期继续计算,应明确传入对应的时间戳:
$firstDate = strtotime('first day of next month');
$nextPaymentDate = date('Y-m-d', strtotime('+1 month', $firstDate));
对于当前规则,最直接的做法仍然是先固定到当月第一天,再增加 1 或 2 个月。这样可以避开月末和闰年带来的日期溢出。
备注:内容仅供参考。
