Symfony Validator 是獨立、可複用的驗證庫,基於 JSR-303 Bean Validation 規範實現。核心優勢:
composer require symfony/validator symfony/property-access symfony/expression-language
// src/Entity/User.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class User {
#[Assert\NotBlank(message: '用戶名不能為空')]
#[Assert\Length(min: 3, max: 20, minMessage: '至少 {{ limit }} 字符', maxMessage: '最多 {{ limit }} 字符')]
#[Assert\Regex(pattern: '/^[a-zA-Z0-9_]+$/', message: '只能包含字母、數字、下劃線')]
public string $username = '';
#[Assert\NotBlank]
#[Assert\Email(message: '郵箱格式不正確')]
public string $email = '';
#[Assert\NotBlank(groups: ['registration'])]
#[Assert\Length(min: 8, minMessage: '密碼至少 8 位', groups: ['registration'])]
#[Assert\Regex(pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', message: '需含大小寫字母和數字', groups: ['registration'])]
public string $plainPassword = '';
#[Assert\Callback]
public function isPasswordLegal(ExecutionContextInterface $context) {
if ($this->plainPassword !== null && str_contains($this->plainPassword, $this->username)) {
$context->buildViolation('密碼不能包含用戶名')
->atPath('plainPassword')
->addViolation();
}
}
}
use Symfony\Component\Validator\Validator\ValidatorInterface;
use App\Entity\User;
class UserController {
public function register(ValidatorInterface $validator, Request $request): JsonResponse {
$user = new User();
$user->username = $request->request->get('username');
$user->email = $request->request->get('email');
$user->plainPassword = $request->request->get('password');
// 分組驗證:註冊場景
$violations = $validator->validate($user, null, ['registration']);
if (count($violations) > 0) {
$errors = [];
foreach ($violations as $v) {
$errors[$v->getPropertyPath()][] = $v->getMessage();
}
return new JsonResponse(['errors' => $errors], 422);
}
// 保存用戶...
return new JsonResponse(['message' => '註冊成功']);
}
}
| 方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| PHP 8 註解 | 類型安全、IDE 支持、緊湊、就近維護 | 需 PHP 8.0+、耦合實體類 | 現代項目首選 |
| YAML | 關注點分離、無運行時開銷、易批量編輯 | 需額外文件、IDE 支持弱 | 大型團隊、實體與驗證分離 |
| XML | 標準化、工具鏈豐富 | 冗長、可讀性差 | 遷移項目、企業規範 |
| PHP 靜態方法 | 純 PHP、動態構建、無註解依賴 | 代碼較多 | 動態約束、庫開發 |
# config/validator/validation.yaml
App\Entity\User:
properties:
username:
- NotBlank: ~
- Length: { min: 3, max: 20 }
- Regex:
pattern: '^[a-zA-Z0-9_]+$'
message: '只能包含字母、數字、下劃線'
email:
- NotBlank: ~
- Email: ~
plainPassword:
- NotBlank: { groups: ['registration'] }
- Length: { min: 8, groups: ['registration'] }
getters:
passwordLegal:
- "True": { message: '密碼不能包含用戶名', groups: ['registration'] }
| 類別 | 約束 | 說明 |
|---|---|---|
| 基礎 | NotBlank | 非空字符串(trim 後不為空) |
| Blank | 為空字符串 | |
| NotNull | 非 null | |
| Null | 為 null | |
| True/False | 布爾值真/假 | |
| Type | 類型檢查(int、float、bool、array、object、string、resource、scalar、iterable、callable) | |
| 字符串 | 郵箱格式(支援 RFC、MX 記錄檢查) | |
| Length | 長度範圍(min、max、charset) | |
| Url | URL 格式 | |
| Regex | 正則匹配 | |
| Ip | IP 地址(v4/v6) | |
| 數字 | Range | 數值範圍(min、max) |
| Positive/Negative | 正數/負數(含零選項) | |
| DivisibleBy | 可被整除 | |
| 日期 | Date/DateTime/Time | 日期時間格式驗證 |
| GreaterThan/LessThan | 比較驗證 | |
| Expression | ExpressionLanguage 表達式 | |
| 集合 | Choice | 枚舉值(choices、multiple、min/max) |
| Collection | 數組結構驗證(fields、allowExtraFields、allowMissingFields) | |
| UniqueEntity | 實體唯一性(需 Doctrine、字段組合) | |
| Count | 可計數對象元素數量 | |
| 文件 | File | 上傳文件(maxSize、mimeTypes、extensions、disallowEmptyMessage) |
| Image | 圖片文件(minWidth、maxWidth、minHeight、maxHeight、allowLandscape/Portrait) | |
| 其他 | Callback | 自定義回調方法/閉包 |
| All | 集合每個元素都滿足 | |
| Valid | 遞歸驗證嵌套對象 |
class User {
#[Assert\NotBlank(groups: ['registration', 'profile_update'])]
public string $email = '';
#[Assert\NotBlank(groups: ['registration'])]
#[Assert\Length(min: 8, groups: ['registration'])]
public string $plainPassword = '';
#[Assert\NotBlank(groups: ['profile_update'])]
public string $currentPassword = '';
}
// 控制器
$violations = $validator->validate($user, null, ['registration']); // 僅驗證 registration 組
$violations = $validator->validate($user, null, ['profile_update']);
$violations = $validator->validate($user); // Default 組(無分組的約束)
class Order {
#[Assert\Valid] // 遞歸驗證 OrderItem
#[Assert\Count(min: 1, minMessage: '訂單至少一項商品')]
public array $items = [];
}
class OrderItem {
#[Assert\NotBlank]
public string $productName = '';
#[Assert\Range(min: 0.01, minMessage: '價格必須大於 0')]
public float $price = 0;
#[Assert\Range(min: 1, minMessage: '數量至少 1')]
public int $quantity = 1;
}
// 1. 約束類
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ChineseMobile extends Constraint {
public string $message = '手機號格式不正確';
public string $pattern = '/^1[3-9]\d{9}$/';
}
// 2. 驗證器
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ChineseMobileValidator extends ConstraintValidator {
public function validate($value, Constraint $constraint) {
if ($value === null || $value === '') return;
if (!preg_match($constraint->pattern, $value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}
// 3. 使用
class User {
#[App\Validator\Constraints\ChineseMobile]
public string $mobile = '';
}
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
class DynamicUser {
public static function loadValidatorMetadata(ClassMetadata $metadata) {
$metadata->addPropertyConstraint('email', new Assert\Email());
// 根據運行時條件動態添加
if (someCondition()) {
$metadata->addPropertyConstraint('phone', new Assert\Regex('/^1[3-9]\d{9}$/'));
}
// 方法級約束
$metadata->addGetterConstraint('isAdult', new Assert\True([
'message' => '必須年滿 18 歲'
]));
}
public function isAdult(): bool {
return $this->age >= 18;
}
}
// FormType
class UserType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('username', TextType::class, [
'constraints' => [
new Assert\NotBlank(),
new Assert\Length(['min' => 3, 'max' => 20]),
],
])
->add('email', EmailType::class)
->add('plainPassword', PasswordType::class, [
'mapped' => false, // 不直接映射到實體
'constraints' => [
new Assert\NotBlank(groups: ['registration']),
new Assert\Length(min: 8, groups: ['registration']),
],
]);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
return $data->getId() ? ['profile_update'] : ['registration', 'Default'];
},
]);
}
}
// Controller
public function edit(Request $request, User $user, EntityManagerInterface $em) {
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 處理密碼加密等
$em->flush();
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', ['form' => $form->createView()]);
}
# translations/validators/zh_CN.yaml
# 覆蓋內置消息
"This value should not be blank": "該字段不能為空"
"This value is not a valid email address": "郵箱地址格式不正確"
"{{ value }} is not a valid {{ type }}": "{{ value }} 不是有效的 {{ type }}"
# 自定義約束消息
chinese_mobile.invalid: "手機號格式不正確"
cache 目錄,避免每次解析註解/YAMLSymfony Validator 以聲明式配置、豐富內置約束、分組驗證、表單無縫集成、國際化支援、高度可擴展 成為 PHP 生態最強大的驗證庫。掌握註解/YAML/XML/PHP 四種配置、核心約束分類、分組/級聯/自定義/動態進階特性、表單集成模式、性能優化策略,即可在任何 PHP 項目(Symfony、Laravel、獨立庫)中構建健壯、可維護、用戶友好的數據驗證層。核心思想:驗證規則與業務邏輯分離、配置即代碼、約束可組合、錯誤消息可國際化。