將分散的驗證邏輯封裝為可復用類,是從過程式邁向面向對象的關鍵一步。本文提供一個現代化、可擴展的 Validator 類實現,涵蓋常見規則、鏈式調用、錯誤收集、數據清洗、魔術引用兼容,並對比主流框架組件的優勢。
<?php
declare(strict_types=1);
namespace App\Validation;
use InvalidArgumentException;
class Validator
{
/** @var array 原始數據 */
private array $data = [];
/** @var array 錯誤消息:字段 => [消息...] */
private array $errors = [];
/** @var array 驗證通過後的清洗數據 */
private array $validated = [];
/** @var array 自定義規則 */
private array $customRules = [];
public function __construct(array $data = [])
{
$this->data = $data;
}
/** 設置待驗證數據 */
public function setData(array $data): self
{
$this->data = $data;
$this->errors = [];
$this->validated = [];
return $this;
}
/** 註冊自定義規則 */
public function addRule(string $name, callable $rule): self
{
$this->customRules[$name] = $rule;
return $this;
}
/** 執行驗證規則鏈 */
public function validate(array $rules): bool
{
foreach ($rules as $field => $fieldRules) {
$value = $this->data[$field] ?? null;
$ruleList = is_array($fieldRules) ? $fieldRules : explode('|', $fieldRules);
foreach ($ruleList as $rule) {
$this->applyRule($field, $value, $rule);
if (isset($this->errors[$field])) {
break; // 該字段首個失敗即停止(可配置為收集所有)
}
}
}
return empty($this->errors);
}
/** 應用單條規則 */
private function applyRule(string $field, mixed $value, string $rule): void
{
// 解析規則參數,如 max:255, regex:/pattern/
[$ruleName, $params] = $this->parseRule($rule);
$method = "validate{$ruleName}";
if (method_exists($this, $method)) {
$this->$method($field, $value, ...$params);
} elseif (isset($this->customRules[$ruleName])) {
$ok = ($this->customRules[$ruleName])($value, ...$params);
if (!$ok) $this->addError($field, "驗證失敗: $ruleName");
} else {
throw new InvalidArgumentException("未知驗證規則: $ruleName");
}
}
private function parseRule(string $rule): array
{
if (str_contains($rule, ':')) {
[$name, $paramStr] = explode(':', $rule, 2);
// 簡單參數分割,複雜參數建議用 JSON 或陣列規則
$params = array_map('trim', explode(',', $paramStr));
return [$name, $params];
}
return [$rule, []];
}
private function addError(string $field, string $message): void
{
$this->errors[$field][] = $message;
}
/** 內建驗證方法 */
private function validateRequired(string $field, mixed $value): void
{
if ($value === null || $value === '' || (is_array($value) && empty($value))) {
$this->addError($field, '此字段為必填');
}
}
private function validateString(string $field, mixed $value): void
{
if (!is_string($value)) $this->addError($field, '必須為字符串');
}
private function validateInt(string $field, mixed $value): void
{
if (!filter_var($value, FILTER_VALIDATE_INT)) $this->addError($field, '必須為整數');
}
private function validateNumeric(string $field, mixed $value): void
{
if (!is_numeric($value)) $this->addError($field, '必須為數字');
}
private function validateEmail(string $field, mixed $value): void
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) $this->addError($field, '郵箱格式不正確');
}
private function validateUrl(string $field, mixed $value): void
{
if (!filter_var($value, FILTER_VALIDATE_URL)) $this->addError($field, 'URL 格式不正確');
}
private function validateRegex(string $field, mixed $value, string $pattern): void
{
if (!preg_match($pattern, (string)$value)) $this->addError($field, '格式不符合要求');
}
private function validateMin(string $field, mixed $value, int $min): void
{
$len = is_string($value) ? mb_strlen($value) : (is_array($value) ? count($value) : $value);
if ($len < $min) $this->addError($field, "最小長度/值為 $min");
}
private function validateMax(string $field, mixed $value, int $max): void
{
$len = is_string($value) ? mb_strlen($value) : (is_array($value) ? count($value) : $value);
if ($len > $max) $this->addError($field, "最大長度/值為 $max");
}
private function validateBetween(string $field, mixed $value, int $min, int $max): void
{
$this->validateMin($field, $value, $min);
if (empty($this->errors[$field])) $this->validateMax($field, $value, $max);
}
private function validateAlphaNum(string $field, mixed $value): void
{
if (!ctype_alnum((string)$value)) $this->addError($field, '只能包含字母和數字');
}
private function validateAlphaDash(string $field, mixed $value): void
{
if (!preg_match('/^[a-zA-Z0-9_-]+$/', (string)$value)) $this->addError($field, '只能包含字母、數字、下劃線、連字符');
}
private function validateChinese(string $field, mixed $value): void
{
if (!preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', (string)$value)) $this->addError($field, '只能包含中文字符');
}
private function validateMobile(string $field, mixed $value): void
{
if (!preg_match('/^1[3-9]\d{9}$/', (string)$value)) $this->addError($field, '手機號格式不正確');
}
private function validateIdCard(string $field, mixed $value): void
{
// 簡易格式,嚴格校驗需專用庫
if (!preg_match('/^\d{17}[\dXx]$/', (string)$value)) $this->addError($field, '身份證號格式不正確');
}
private function validateIp(string $field, mixed $value): void
{
if (!filter_var($value, FILTER_VALIDATE_IP)) $this->addError($field, 'IP 地址格式不正確');
}
private function validateJson(string $field, mixed $value): void
{
json_decode((string)$value);
if (json_last_error() !== JSON_ERROR_NONE) $this->addError($field, 'JSON 格式不正確');
}
private function validateDate(string $field, mixed $value, string $format = 'Y-m-d'): void
{
$dt = DateTime::createFromFormat($format, (string)$value);
if (!$dt || $dt->format($format) !== (string)$value) $this->addError($field, "日期格式應為 $format");
}
/** 獲取錯誤 */
public function errors(): array
{
return $this->errors;
}
public function firstError(string $field): ?string
{
return $this->errors[$field][0] ?? null;
}
/** 獲取驗證通過的清洗數據 */
public function validated(): array
{
if (empty($this->validated)) {
// 簡單策略:僅返回通過驗證的字段原始值
// 實際項目中應結合清洗器(如 trim, strip_tags, 類型轉換)
foreach (array_keys($this->data) as $field) {
if (!isset($this->errors[$field])) {
$this->validated[$field] = $this->data[$field];
}
}
}
return $this->validated;
}
/** 靜態工廠方法 */
public static function make(array $data): self
{
return new self($data);
}
}
<?php
use App\Validation\Validator;
$data = [
'username' => ' zhangsan ',
'email' => 'zhangsan@example.com',
'password' => 'Secret123!',
'mobile' => '13800138000',
'age' => '25',
];
$validator = Validator::make($data);
// 定義規則(陣列或管道字符串)
$rules = [
'username' => ['required', 'string', 'min:3', 'max:20', 'alpha_dash'],
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:8', 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).+$/'],
'mobile' => ['required', 'mobile'],
'age' => ['required', 'int', 'between:1,120'],
];
if ($validator->validate($rules)) {
$clean = $validator->validated();
// 入庫、業務邏輯...
echo "驗證通過: " . json_encode($clean, JSON_UNESCAPED_UNICODE);
} else {
echo "驗證失敗: " . json_encode($validator->errors(), JSON_UNESCAPED_UNICODE);
}
驗證通過後,往往需要統一格式。可在類中添加 sanitize() 方法或獨立 Sanitizer 類:
public function sanitized(): array
{
$result = [];
foreach ($this->validated() as $field => $value) {
$result[$field] = match (true) {
is_string($value) => trim(strip_tags($value)),
is_numeric($value) => (float)$value,
is_bool($value) => (bool)$value,
default => $value,
};
}
return $result;
}
// 在入口文件或框架啟動時統一處理
if (ini_get('magic_quotes_gpc')) {
function stripslashes_deep(mixed $value): mixed
{
return is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value);
}
$_GET = stripslashes_deep($_GET);
$_POST = stripslashes_deep($_POST);
$_COOKIE = stripslashes_deep($_COOKIE);
$_REQUEST = stripslashes_deep($_REQUEST);
}
| 特性 | 本類 | Laravel Validation | Symfony Validator | Respect/Validation |
|---|---|---|---|---|
| 規則定義 | 陣列/字符串 | 字符串/陣列/規則對象 | 註解/屬性/Xml/Yaml | 鏈式調用 | 錯誤消息 | 硬編碼/可擴展 | 語言包、自定義 | 翻譯組件、自定義 | 自定義 |
| 異步/遠程驗證 | 不支持 | 支持 | 支持 | 不支持 |
| 依賴注入友好 | 需手動 | 原生支持 | 原生支持 | 支持 |
| 學習曲線 | 低 | 中 | 中高 | 低 |
illuminate/validation(Laravel 組件獨立包)或 symfony/validator,獲得完整生態(翻譯、測試工具、擴展點)。respect/validation 或 rakit/validation,輕量且鏈式 API 適合 API 參數驗證。封裝驗證類能統一規則、集中錯誤處理、分離業務邏輯。本文實現採用策略模式(規則方法映射)、鏈式調用、錯誤優先收集,易於擴展自定義規則。隨著專案增長,平滑遷移至成熟組件是明智選擇。核心原則永遠是:不信任任何輸入,驗證在邊界,清洗在入口,類型在聲明。