看现象 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-07-31")))); // string(10) "2020-08-31" 符合预期 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-05-31")))); // string(10) "2020-07-01" 不符合预期,预期 2020-06-30 var_dump(date("Y-m-d", strtotime("-1 month", strtotime("2020-02-29")))); // string(10) "2020-01-29" 符合预期 var_dump(date("Y-m-d", strtotime("-1 month", strtotime("2020-03-31")))); // string(10) "2020-03-02" 不符合预期,预期 2020-02-29 // Carbon\Carbon Carbon::parse("2020-07-31")->addMonth()->toDateString(); // "2020-08-31" Carbon::parse("2020-05-31")->addMonth()->toDateString(); // "2020-07-01" Carbon::parse("2020-02-29")->subMonth()->toDateString(); // "2020-01-29" Carbon::parse("2020-03-31")->subMonth()->toDateString(); // "2020-03-02" // 结果与 strtotime 一致。 原因 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-05-31")))); // string(10) "2020-07-01" date 内部的处理逻辑:
2020-05-31 做 +1 month 也就是 2020-06-31。 再做日期规范化,因为没有 06-31,所以 06-31 就等于了 07-01。 var_dump(date("Y-m-d", strtotime("2020-06-31"))); // string(10) "2017-07-01" var_dump(date("Y-m-d", strtotime("next month", strtotime("2017-01-31")))); // string(10) "2017-03-03" var_dump(date("Y-m-d", strtotime("last month", strtotime("2017-03-31")))); // string(10) "2017-03-03" 解决方案 var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-31")))); // string(10) "2017-02-28" var_dump(date("Y-m-d", strtotime("first day of +1 month", strtotime("2017-08-31")))); // string(10) "2017-09-01" // 但要注意短语的含义: var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-01")))); // string(10) "2017-02-28" 如果使用 Carbon\Carbon 可以用 subMonthNoOverflow 与 addMonthNoOverflow 防止进位:
...