日期時間驗證看似簡單,實則暗藏格式檢查、邊界值驗證(閏年、大小月)、時區處理、字串解析容錯等陷阱。本文對比 正則表達式、checkdate()、DateTime 類別、strtotime() 四大流派,給出生產環境可直接用的最佳實踐方案。
| 方法 | 格式檢查 | 邊界驗證 | 效能 | 適用場景 |
|---|---|---|---|---|
| 正則表達式 | ✅ 強 | ❌ 極難(需超長正則) | 中 | 固定格式預檢 |
| checkdate() | ❌ 需自行拆解 | ✅ 內建閏年/月份 | 高 | 純日期驗證 |
| DateTime::createFromFormat() | ✅ 強 | ✅ 自動驗證 | 中 | 嚴格格式+驗證 |
| strtotime() | ⚠️ 寬鬆解析 | ✅ 隱含驗證 | 高 | 多格式兼容、快速判斷 |
/**
* 驗證日期時間字串是否合法
* 支援格式:YYYY-MM-DD HH:II:SS、YYYY-MM-DD、YYYY/MM/DD 等常見格式
* @return bool
*/
function isValidDateTime(string $dt, string $format = 'Y-m-d H:i:s'): bool {
// 1. 正則預檢:快速攔截明顯格式錯誤
if (!preg_match('/^\d{4}[-\/]\d{2}[-\/]\d{2}( \d{2}:\d{2}:\d{2})?$/', $dt)) {
return false;
}
// 2. strtotime 解析:回傳 false 表示無法解析(含邊界錯誤如 2023-02-30)
$ts = strtotime($dt);
if ($ts === false) {
return false;
}
// 3. 往返驗證:解析後格式化回字串,比對原始輸入(防止 strtotime 過度容錯,如 "2023-13-01" → 2024-01-01)
$back = date($format, $ts);
// 正規化原始輸入格式以便比對
$normInput = date($format, strtotime($dt));
return $back === $normInput;
}
// 測試
var_dump(isValidDateTime('2023-02-28 12:30:45')); // true
var_dump(isValidDateTime('2023-02-30')); // false (2月無30日)
var_dump(isValidDateTime('2023-13-01')); // false
var_dump(isValidDateTime('2023/12/31')); // true (支援斜線)
var_dump(isValidDateTime('not-a-date')); // false
function isValidDateTimeStrict(string $dt, string $format = 'Y-m-d H:i:s'): bool {
$dtObj = DateTime::createFromFormat($format, $dt);
if (!$dtObj) return false;
// 檢查是否有解析錯誤(如多餘字元、邊界錯誤)
$errors = DateTime::getLastErrors();
return $errors['warning_count'] === 0 && $errors['error_count'] === 0;
}
// 使用
var_dump(isValidDateTimeStrict('2023-02-28 12:30:45')); // true
var_dump(isValidDateTimeStrict('2023-02-30')); // false
var_dump(isValidDateTimeStrict('2023-02-28 12:30')); // false (格式不符 H:i:s)
function isValidDate(int $year, int $month, int $day): bool {
return checkdate($month, $day, $year);
}
// 從字串拆解驗證
function isValidDateFromString(string $dateStr): bool {
if (!preg_match('/^(\d{4})[-\/](\d{2})[-\/](\d{2})$/', $dateStr, $m)) {
return false;
}
return checkdate((int)$m[2], (int)$m[3], (int)$m[1]);
}
| 坑點 | 現象 | 解決 |
|---|---|---|
| strtotime 過度容錯 | '2023-13-01' → 解析為 2024-01-01 | 往返驗證(方案一步驟 3)或用 DateTime::createFromFormat |
| 時區導致日期偏移 | UTC 晚間 strtotime('2023-01-01') 可能落在前一日 | 驗證前設定 date_default_timezone_set('Asia/Taipei') 或用 DateTimeImmutable 指定時區 |
| 閏年 2/29 驗證失效 | checkdate(2,29,2023) 應為 false | checkdate/DateTime 內建正確處理 |
| HTML5 datetime-local 格式 | '2023-12-31T23:59' (T 分隔) | 格式字串用 'Y-m-d\TH:i' 或預處理替換 T 為空格 |
class DateTimeValidator {
public static function validate(string $input, string $format = 'Y-m-d H:i:s'): array {
$errors = [];
// 1. 必填
if ($input === '') {
$errors[] = '日期時間不可為空';
return ['valid' => false, 'errors' => $errors];
}
// 2. 格式正則
if (!preg_match('/^\d{4}[-\/]\d{2}[-\/]\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?$/', $input)) {
$errors[] = '格式錯誤,請使用 YYYY-MM-DD HH:II:SS';
return ['valid' => false, 'errors' => $errors];
}
// 3. 正規化分隔符
$normalized = str_replace(['/', 'T'], ['-', ' '], $input);
// 4. strtotime + 往返驗證
$ts = strtotime($normalized);
if ($ts === false) {
$errors[] = '日期時間不存在(如 2 月 30 日)';
return ['valid' => false, 'errors' => $errors];
}
$back = date($format, $ts);
$normInput = date($format, $ts);
if ($back !== $normInput) {
$errors[] = '日期時間超出合理範圍';
return ['valid' => false, 'errors' => $errors];
}
return ['valid' => true, 'timestamp' => $ts, 'formatted' => $back];
}
}
// 使用
$result = DateTimeValidator::validate($_POST['birthday'] ?? '');
if ($result['valid']) {
$dt = new DateTime('@' . $result['timestamp']);
$dt->setTimezone(new DateTimeZone('Asia/Taipei'));
// 存入資料庫...
}
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE 拒絕 0000-00-00、2023-00-00 等非法值。嚴格業務(生日、合約日期)用 DateTime::createFromFormat();兼容多格式、高吞吐用 正則+strtotime+往返驗證;純日期最輕量用 checkdate()。切記:永遠在應用層驗證,不要只依賴資料庫報錯,才能給使用者友善回饋、保障資料品質。