報表統計、帳單週期、排程任務常需「本月第一天/最後一天」、「上月範圍」、「下月範圍」。PHP DateTime 類別配合 modify() 相對時間格式,能以零數學運算、自動處理閏年/月份天數差異的方式優雅解決。本文提供可直接使用的工具函數與最佳實踐。
<?php
/**
* 取得指定月份的首尾日期 (DateTimeImmutable 物件)
*
* @param string|DateTimeInterface $base 基準日期,預設 today
* @param int $month_offset 月份偏移:0=本月, -1=上月, +1=下月, ...
* @return array ['start' => DateTimeImmutable, 'end' => DateTimeImmutable]
*/
function getMonthRange($base = 'today', int $month_offset = 0): array {
$dt = $base instanceof DateTimeInterface
? DateTimeImmutable::createFromInterface($base)
: new DateTimeImmutable($base);
// 1. 移動到目標月份的第一天 00:00:00
$start = $dt->modify("first day of {$month_offset} month")
->setTime(0, 0, 0);
// 2. 最後一天 23:59:59
$end = $start->modify('last day of this month')
->setTime(23, 59, 59);
return ['start' => $start, 'end' => $end];
}
/**
* 格式化輸出常用格式
*/
function formatRange(array $range, string $format = 'Y-m-d'): array {
return [
'start' => $range['start']->format($format),
'end' => $range['end']->format($format),
];
}
// === 使用範例 ===
// 本月
$now = new DateTimeImmutable();
echo "本月: " . formatRange(getMonthRange($now, 0))['start'] . " ~ " . formatRange(getMonthRange($now, 0))['end'] . "\n";
// 2024-01-01 ~ 2024-01-31
// 上月
echo "上月: " . formatRange(getMonthRange($now, -1))['start'] . " ~ " . formatRange(getMonthRange($now, -1))['end'] . "\n";
// 2023-12-01 ~ 2023-12-31
// 下月
echo "下月: " . formatRange(getMonthRange($now, +1))['start'] . " ~ " . formatRange(getMonthRange($now, +1))['end'] . "\n";
// 2024-02-01 ~ 2024-02-29 (閏年自動處理)
// 指定日期為基準 (如帳單日)
$billing = new DateTimeImmutable('2024-03-15');
echo "帳單月 (3/15 基準): " . formatRange(getMonthRange($billing, 0))['start'] . " ~ " . formatRange(getMonthRange($billing, 0))['end'] . "\n";
// 2024-03-01 ~ 2024-03-31
?>
| 格式字串 | 含義 | 範例 |
|---|---|---|
first day of this month | 本月 1 號 00:00 | 2024-01-01 00:00:00 |
last day of this month | 本月最後一天 23:59:59 | 2024-01-31 23:59:59 |
first day of -1 month | 上月 1 號 | 2023-12-01 00:00:00 |
first day of +1 month | 下月 1 號 | 2024-02-01 00:00:00 |
first day of +3 months | 三個月後的月份第一天 | |
last day of -1 month | 上月最後一天 | |
midnight / today | 當天 00:00:00 | |
yesterday / tomorrow | 前一天/後一天 00:00 |
| 特性 | strtotime() + date() | DateTimeImmutable |
|---|---|---|
| 型別安全 | 字串/整數,易出錯 | 強型別物件,IDE 提示 |
| 不可變性 | 無 | 修改返回新物件,避免副作用 |
| 時區處理 | 依賴 date_default_timezone_set | 物件內建時區,可單獨設定 |
| 閏年/月份邊界 | 手動計算易錯 | 內建邏輯自動正確 |
| 格式化 | date('Y-m-d', $ts) | $dt->format('Y-m-d') |
| 運算 | strtotime('+1 month', $ts) | $dt->modify('+1 month') |
strtotime('+1 month', '2024-01-31') → 2024-03-03:因 2 月無 31 號向後溢位。解決:用 first day of +1 month。DateTime::modify() 會改變原物件。解決:用 DateTimeImmutable 或 clone。new DateTimeImmutable('now', new DateTimeZone('Asia/Taipei'))。DATETIME 需 Y-m-d H:i:s,DATE 需 Y-m-d。<?php
$range = getMonthRange('today', -1); // 上月
$sql = "SELECT * FROM orders
WHERE created_at >= :start AND created_at <= :end";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'start' => $range['start']->format('Y-m-d H:i:s'),
'end' => $range['end']->format('Y-m-d H:i:s'),
]);
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
使用 DateTimeImmutable 搭配 first day of ... month / last day of ... month 相對格式,即可零 Bug、零數學、自動處理閏年地取得任意月份首尾日期。封裝為 getMonthRange() 等工具函數,專案中統一呼叫,徹底消除日期邊界 Bug。