首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 表單驗證類封裝:正則驗證、數據清洗與安全最佳實踐

     編輯:PHP 相關知識

前言

將分散的驗證邏輯封裝為可復用類,是從過程式邁向面向對象的關鍵一步。本文提供一個現代化、可擴展的 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 ValidationSymfony ValidatorRespect/Validation
規則定義陣列/字符串字符串/陣列/規則對象註解/屬性/Xml/Yaml鏈式調用
錯誤消息硬編碼/可擴展語言包、自定義翻譯組件、自定義自定義
異步/遠程驗證不支持支持支持不支持
依賴注入友好需手動原生支持原生支持支持
學習曲線低中中高低

遷移建議

  • 微型專案/舊系統維護:本類足夠,複製即用,零依賴。
  • 中大型新專案:直接使用 illuminate/validation(Laravel 組件獨立包)或 symfony/validator,獲得完整生態(翻譯、測試工具、擴展點)。
  • API 專項:考慮 respect/validation 或 rakit/validation,輕量且鏈式 API 適合 API 參數驗證。

總結

封裝驗證類能統一規則、集中錯誤處理、分離業務邏輯。本文實現採用策略模式(規則方法映射)、鏈式調用、錯誤優先收集,易於擴展自定義規則。隨著專案增長,平滑遷移至成熟組件是明智選擇。核心原則永遠是:不信任任何輸入,驗證在邊界,清洗在入口,類型在聲明。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved