strtotime() 是 PHP 核心日期時間處理函數,能將英文文本日期解析為 Unix 時間戳。但其「相對月份計算」行為常讓開發者踩坑——特別是 strtotime('-1 month') 在月底日期會產生非預期結果。本文從源碼層面剖析原理,並給出最佳實踐。
| 語法 | 說明 | 示例輸出(基準 2011-03-31) |
|---|---|---|
feb / February | 指定月份的今天,不存在則順延 | 2011-03-03(2月無31日,順延至3月3日) |
first sunday | 當月第一個星期日 | 2011-01-02 |
previous sunday | 指定日期前最近的星期日 | 2011-01-30(基準 2011-02-01) |
last day | 當月最後一天 | 2000-02-29(閏年) |
back of 24 | 指定小時後 15 分(24 視為翌日凌晨) | 2011-02-02 00:15:00 |
front of 0 | 指定小時前 45 分(0 視為前日 23:45) | 2011-01-31 23:45:00 |
PHP 使用 re2c 詞法分析器解析日期字符串。核心結構體:
timelib_time:存儲年月日時分秒、時區、相對偏移量(relative 字段)。timelib_rel_time:相對時間偏移(年/月/日/時/分/秒、星期行為、first/last 標記等)。解析 yesterday 時,匹配規則執行:
'yesterday' { TIMELIB_INIT; TIMELIB_HAVE_RELATIVE(); s->time->relative.d = -1; return TIMELIB_RELATIVE; }
最終由 timelib_fill_holes() 填充缺失字段,timelib_update_ts() 計算最終時間戳。
當輸入 -1 month,解析器識別 relnumber = -1、reltextunit = month,執行:
case TIMELIB_MONTH: s->time->relative.m += amount * multiplier; break; // m = -1
關鍵點:此處僅修改「相對月份偏移量」,不觸碰日字段。後續計算時間戳時,若基準日為 31 號、目標月無 31 日,PHP 採用「順延到下月對應日期」策略(如 3 月 31 日 -1 月 → 2 月 31 日 → 3 月 3 日)。這符合數學定義,但違背業務直覺(「上個月同一天」)。
// 方案 1:先取上月 1 號,再加 (當前日-1) 天
$firstDayThisMonth = strtotime('first day of this month');
$firstDayLastMonth = strtotime('-1 month', $firstDayThisMonth);
$targetDay = min(date('j'), date('t', $firstDayLastMonth)); // 避免 31→30/28/29
$result = strtotime("+{$targetDay} days", $firstDayLastMonth);
// 方案 2:DateTime 類(推薦,PHP 5.3+)
$dt = new DateTime('2011-03-31');
$dt->modify('first day of last month');
$dt->modify('+' . (min($dt->format('j'), $dt->format('t')) - 1) . ' days');
echo $dt->format('Y-m-d'); // 2011-02-28
strtotime() 的相對月份計算是「月份偏移優先,日期順延」,而非「日曆月份倒推」。業務邏輯涉及「上個月同一天」、「下個月同一天」時,**請改用 DateTime::modify() 配合 first day of 策略**,或封裝工具函數統一處理,避免月底日期產生邏輯錯誤。