Learning Objectives
- 說明 Unix 時間戳記是什麼,用 time()、mktime() 取得時間戳記,再用 date() 排成想要的格式
- 說明同一個時間戳記在不同時區會顯示成不同的當地時間,並用 date_default_timezone_set() 設定時區
- 用 strtotime() 解讀「+1 day」「next monday」這類描述,並檢查回傳 false 的情況
- 分辨 DateTime 與 DateTimeImmutable:modify 會改到原物件,還是回傳新物件
- 用 DateInterval 加減時間、用 diff() 算年齡與倒數天數,並避開「月底加一個月」的陷阱
網站到處都是時間
留言的發表時間、訂單的建立時間、報名截止前還剩幾天、會員今年幾歲……幾乎每個網站都要處理日期和時間。
日期看起來簡單,其實有不少陷阱:每個月的天數不同、有閏年、世界各地有時差,有些地方還有日光節約時間。
PHP 內建的日期函式與 DateTime 類別已經幫你處理好這些規則,你要學的是怎麼表示時間、怎麼換算、怎麼顯示。
電腦裡最常用的時間表示法是 Unix 時間戳記(Unix timestamp):從 1970-01-01 00:00:00 UTC 起算經過了幾秒,是一個整數。
| 時間(UTC) | 時間戳記 |
|---|---|
| 1970-01-01 00:00:00 | 0 |
| 1970-01-02 00:00:00 | 86400(一天 = 24 × 60 × 60 秒) |
| 2026-09-01 09:00:00 | 1788253200 |
時間戳記不受時區影響:地球上同一個瞬間,不論在臺北還是紐約,時間戳記都相同;只有「顯示成當地時間」時才需要時區。
CodeLab 的設定:真正的 PHP 每次執行,
time()都會拿到當下的時間。為了讓每個人看到的結果相同, CodeLab 的「現在」固定是 2026-09-01 09:00:00 UTC(星期二),預設時區是 UTC。在你自己的電腦上執行,結果會隨當下的時間改變。
時間戳記與 date()
date(格式, 時間戳記) 把時間戳記排成字串;第二個參數省略時用現在的時間。時間戳記是整數,所以「明天」可以直接加上一天的秒數(第 9 行)。mktime(時, 分, 秒, 月, 日, 年) 反過來,把年月日時分秒換成時間戳記,注意參數的順序是時分秒在前、月日年在後。
date() 的格式字元
date() 的格式字串裡,有特殊意義的英文字母會換成對應的值,其他字元(-、/、:、空白、中文)原樣保留。
常用的格式字元(以 2026-09-01 09:00 星期二為例):
| 字元 | 意思 | 範例 |
|---|---|---|
Y |
四位數年份 | 2026 |
m |
月,補零 | 09 |
n |
月,不補零 | 9 |
d |
日,補零 | 01 |
j |
日,不補零 | 1 |
H |
時,24 小時制,補零 | 09 |
i |
分,補零 | 00 |
s |
秒,補零 | 00 |
D |
星期,三個字母 | Tue |
l |
星期,英文全名(小寫的 L) | Tuesday |
N |
星期,數字,星期一 = 1 … 星期日 = 7 | 2 |
w |
星期,數字,星期日 = 0 … 星期六 = 6 | 2 |
t |
這個月有幾天 | 30 |
L |
是否為閏年,是 = 1、否 = 0 | 0 |
a |
小寫 am/pm | am |
A |
大寫 AM/PM | AM |
常見的組合:資料庫的 DATETIME 欄位用 'Y-m-d H:i:s';臺灣習慣的顯示方式可以寫 'Y/m/d H:i'。
要在格式裡放一般的英文字母,在前面加反斜線跳脫,例如 date('\T\o\d\a\y: Y-m-d')(用單引號字串)。
常見的錯誤是把月和分搞混:m 是月、i 是分(minute 的 m 已經被月份用掉了)。
時區:同一個瞬間,各地的時鐘不同
date() 會把時間戳記換成「某個時區的當地時間」,用的是預設時區。CodeLab 與很多伺服器的預設時區是 UTC(世界協調時間);
臺灣比 UTC 快 8 小時(UTC+8),所以 UTC 的 09:00 就是臺北的 17:00。
date_default_timezone_set('Asia/Taipei'); // 之後的 date()、mktime() 都用臺北時間
echo date_default_timezone_get(); // Asia/Taipei
時區用 IANA 時區名稱(也叫 tz database 名稱)表示,格式是「洲/城市」:Asia/Taipei、Asia/Tokyo、America/New_York、Europe/London。
不要用 +08:00 這種固定的時差代替時區名稱:美國、歐洲有日光節約時間,夏天和冬天的時差不同,時區名稱會自動套用正確的規則。
為什麼伺服器常設成 UTC? 網站的使用者可能在不同國家,伺服器也可能搬到別的地區。實務上常見的作法是: 資料庫一律存 UTC 時間(或時間戳記),只在顯示給使用者看的時候才換成使用者的時區。這樣比較時間先後、計算時間差都不會被時差干擾。
也可以在 php.ini 設定 date.timezone = Asia/Taipei,就不用每支程式都呼叫 date_default_timezone_set()。
切換時區
注意 $ts 從頭到尾都沒有改變,改變的只有「顯示成哪個時區的時間」。第 11~14 行用同一組「9 月 1 日 08:00」呼叫 mktime() 兩次:時區不同,得到的時間戳記也不同。
想一想:時間戳記、格式與時區
下面的程式會印出什麼?
<?php
$t = mktime(0, 0, 0, 3, 5, 2026);
echo date('Y-n-j', $t);
下面的程式會印出什麼?
<?php
date_default_timezone_set('Asia/Taipei');
echo date('Y-m-d H:i', 0);
strtotime:用英文描述日期
strtotime(描述, 基準時間戳記) 把一段英文的日期描述轉成時間戳記。第二個參數省略時,以「現在」為基準。
| 描述 | 意思 |
|---|---|
'2026-12-25 18:30' |
指定的日期時間(用目前的時區解讀) |
'+1 day'、'-2 weeks'、'+1 week 2 hours' |
從基準時間往後或往前 |
'tomorrow'、'yesterday'、'today' |
明天、昨天、今天的 00:00 |
'next monday'、'last friday' |
下一個星期一、上一個星期五(00:00) |
'first day of next month'、'last day of next month' |
下個月的第一天、最後一天 |
'first friday of october 2026' |
2026 年 10 月的第一個星期五 |
兩件要注意的事:
- 看不懂的描述回傳
false,例如中文「明天」、拼錯的next fryday。拿使用者輸入的字串呼叫 strtotime 時,一定要用=== false檢查; 直接把 false 交給date(),會被當成 0,顯示成 1970 年。 - 不存在的日期不一定回傳 false:
'2026-02-30'會「溢位」成 3 月 2 日。要確認使用者輸入的年月日真的存在,用後面介紹的checkdate()。
strtotime 的各種寫法
每一行都以 $base(臺北時間 2026-09-01 10:30,星期二)為基準。比較第 7 行的 +1 day(保留 10:30)和第 9 行的 tomorrow(變成 00:00)。
DateTime 與 DateTimeImmutable
時間戳記加減秒數很方便,但遇到「加一個月」「兩個日期差幾年幾個月」就麻煩了。PHP 內建兩個類別來表示一個日期時間:
DateTime 與 DateTimeImmutable(PHP 5.5 加入)。自己寫類別是第 14 課的內容,這一課先把它們當成「內建的物件」使用,只需要兩個寫法:
new 類別名稱(參數):建立一個物件,例如$d = new DateTime('2026-09-01');(不給參數就是現在)$物件->方法(參數):呼叫這個物件的方法,例如$d->format('Y-m-d')
| 方法 | 作用 |
|---|---|
format('Y-m-d') |
排成字串,格式字元和 date() 相同 |
modify('+1 day') |
用 strtotime 的寫法調整時間 |
add(new DateInterval('P1D'))、sub(…) |
加上、減去一段時間 |
diff($另一個) |
算兩個時間的差,回傳 DateInterval |
setTimezone(new DateTimeZone('Asia/Taipei')) |
換成另一個時區顯示 |
getTimestamp() |
取得時間戳記 |
兩個類別的方法幾乎一樣,差別在 modify、add、sub、setTimezone 會不會改到原本的物件:
DateTime(可變) |
DateTimeImmutable(不可變) |
|
|---|---|---|
$b = $a->modify('+1 day') |
改到 $a,再回傳 $a 自己 |
$a 不變,回傳一個新的物件 |
| 適合 | 很少用到 | 建議預設使用 |
物件和陣列不同:$b = $a 不會複製物件,兩個變數指向同一個物件(第 14 課詳細說明)。
用 DateTime 時,一個不小心就會改到別處也在用的日期;DateTimeImmutable 不會有這個問題。
可變與不可變
看右邊的物件:第 4 行執行完,$start 和 $end 的箭頭指向同一個 DateTime;第 9 行執行完,$e2 指向另一個新的 DateTimeImmutable。
DateInterval:一段時間
DateInterval 表示「一段時間」,建立時用 ISO 8601 的期間寫法:以 P(period)開頭,日期部分接著寫數字與單位;
有時、分、秒時,先寫 T(time)再接時間部分。
| 寫法 | 意思 |
|---|---|
P1D |
1 天 |
P2W |
2 週 |
P1M2D |
1 個月又 2 天 |
P1Y |
1 年 |
PT36H |
36 小時 |
PT1H30M |
1 小時 30 分(T 後面的 M 是分) |
P1DT12H |
1 天又 12 小時 |
$a->diff($b) 回傳從 $a 到 $b 的 DateInterval,常用的屬性:
| 屬性 | 意思 |
|---|---|
->y、->m、->d |
相差幾年、幾個月、幾天(例如 20 年 9 個月 17 天,用來算年齡) |
->h、->i、->s |
不滿一天的時、分、秒 |
->days |
相差的總天數(倒數幾天用這個) |
->invert |
$b 比 $a 早時是 1,否則是 0(->days 一律是正數) |
format() 也可以排成字串:%y 年、%m 月、%d 日、%a 總天數、%R 正負號。注意這裡的格式字元前面要加 %,和 date() 不同。
驗證使用者輸入的日期是否存在,用 checkdate(月, 日, 年):checkdate(2, 29, 2026) 是 false(2026 年不是閏年)。
加減時間、算年齡與倒數
$d 是 DateTimeImmutable,第 4~6 行的 add、sub 都回傳新物件,$d 一直是 9 月 1 日。第 18 行是本課最重要的陷阱:1 月 31 日加一個月。
月底加一個月的陷阱
PHP 計算「+1 month」的方式是月份加 1、日期不變,如果那個月沒有這一天,就把多出來的天數往後溢位:
| 起點 | +1 month |
原因 |
|---|---|---|
| 2026-01-31 | 2026-03-03 | 2 月 31 日不存在,2 月只有 28 天,多 3 天 |
| 2026-03-31 | 2026-05-01 | 4 月只有 30 天,多 1 天 |
| 2026-01-15 | 2026-02-15 | 沒有問題 |
月費、每月固定日扣款、「一個月後到期」這類功能都會踩到。常見的處理方式:
- 要「下個月的最後一天」:寫
'last day of next month' - 要「下個月的同一天,沒有就用月底」:先算
'first day of next month',再用min(原本的日, 那個月的天數 t)決定日期 - 訂單到期日改用固定天數(
P30D)計算
想一想:DateTime 的加減
下面的程式會印出什麼?
<?php
$a = new DateTime('2026-09-01');
$b = $a;
$b->modify('+1 day');
echo $a->format('d');
下面的程式會印出什麼?
<?php
$d = new DateTimeImmutable('2026-03-31');
echo $d->modify('+1 month')->format('m-d');
下面的程式會印出什麼?
<?php
$a = new DateTimeImmutable('2026-12-25');
$b = new DateTimeImmutable('2026-12-01');
$i = $a->diff($b);
echo $i->days, ' ', $i->invert;
動手做:活動倒數
社團有四個活動。以今天(CodeLab 的今天是 2026-09-01)為準,每個活動印一行:已經過了印「活動名稱:已結束」;就是今天印「活動名稱:就是今天」;還沒到印「活動名稱:還有 N 天(星期縮寫)」,星期縮寫用格式字元 D。
重點回顧
- 時間戳記是從 1970-01-01 00:00:00 UTC 起算的秒數,和時區無關;
time()取得現在,mktime(時, 分, 秒, 月, 日, 年)指定一個時間 date(格式, 時間戳記)依預設時區排成字串:Y-m-d H:i:s最常用;m是月、i是分,n、j不補零,D/l是星期,N/w是星期數字,t是當月天數,L是閏年- 時區用 IANA 名稱(
Asia/Taipei),date_default_timezone_set()設定;伺服器與資料庫常用 UTC,顯示時才換成使用者的時區 strtotime()看得懂+1 day、next monday、last day of next month;看不懂回傳 false,要用=== false檢查DateTime的 modify/add/sub 會改到原物件,DateTimeImmutable回傳新物件;建議預設用 Immutable,並接住回傳值DateInterval('P1M2D')、('PT36H')表示一段時間;diff()的->y算年齡、->days算倒數天數checkdate(月, 日, 年)驗證日期是否存在;1/31 加一個月是 3/3,要月底請寫last day of next month