表单验证 #
一、验证概述 #
1.1 验证组件 #
Symfony验证组件提供强大的数据验证功能:
text
┌─────────────────────────────────────────────────────┐
│ 验证功能 │
├─────────────────────────────────────────────────────┤
│ • 内置验证约束 │
│ • 自定义验证规则 │
│ • 验证组 │
│ • 错误消息定制 │
│ • 实体验证 │
│ • 表单验证集成 │
└─────────────────────────────────────────────────────┘
1.2 安装验证组件 #
bash
# 安装验证组件
composer require validator
二、实体约束 #
2.1 基本约束 #
php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
#[Assert\NotBlank(message: '姓名不能为空')]
#[Assert\Length(
min: 2,
max: 100,
minMessage: '姓名至少{{ limit }}个字符',
maxMessage: '姓名最多{{ limit }}个字符'
)]
private ?string $name = null;
#[ORM\Column(length: 180, unique: true)]
#[Assert\NotBlank(message: '邮箱不能为空')]
#[Assert\Email(message: '邮箱格式不正确')]
#[Assert\Length(max: 180)]
private ?string $email = null;
#[ORM\Column]
#[Assert\NotBlank(message: '密码不能为空')]
#[Assert\Length(min: 8, minMessage: '密码至少{{ limit }}个字符')]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
message: '密码必须包含大小写字母和数字'
)]
private ?string $password = null;
#[ORM\Column]
#[Assert\Range(
min: 0,
max: 150,
notInRangeMessage: '年龄必须在{{ min }}到{{ max }}之间'
)]
private int $age = 0;
}
2.2 字符串约束 #
php
<?php
use Symfony\Component\Validator\Constraints as Assert;
class Article
{
#[Assert\NotBlank]
#[Assert\Length(min: 5, max: 200)]
private ?string $title = null;
#[Assert\NotBlank]
#[Assert\Length(min: 10)]
private ?string $content = null;
#[Assert\Url(message: '请输入有效的URL')]
private ?string $website = null;
#[Assert\Regex(
pattern: '/^[a-zA-Z0-9-]+$/',
message: '只能包含字母、数字和横线'
)]
private ?string $slug = null;
#[Assert\Language(message: '请输入有效的语言代码')]
private ?string $language = null;
#[Assert\Locale(message: '请输入有效的区域设置')]
private ?string $locale = null;
#[Assert\Country(message: '请输入有效的国家代码')]
private ?string $country = null;
}
2.3 数字约束 #
php
<?php
class Product
{
#[Assert\Positive(message: '价格必须大于0')]
private float $price = 0;
#[Assert\PositiveOrZero(message: '库存不能为负数')]
private int $stock = 0;
#[Assert\Range(min: 1, max: 100)]
private int $quantity = 1;
#[Assert\LessThan(value: 1000, message: '不能超过1000')]
private float $amount = 0;
#[Assert\GreaterThan(value: 0, message: '必须大于0')]
private float $discount = 0;
#[Assert\LessThanOrEqual(propertyPath: 'stock')]
private int $orderQuantity = 0;
}
2.4 日期约束 #
php
<?php
class Event
{
#[Assert\NotBlank]
#[Assert\Date(message: '请输入有效的日期')]
private ?\DateTimeInterface $startDate = null;
#[Assert\NotBlank]
#[Assert\Date]
private ?\DateTimeInterface $endDate = null;
#[Assert\GreaterThan(propertyPath: 'startDate', message: '结束日期必须晚于开始日期')]
private ?\DateTimeInterface $endDate = null;
#[Assert\Range(
min: 'now',
max: '+1 year',
minMessage: '日期不能早于今天',
maxMessage: '日期不能超过一年'
)]
private ?\DateTimeInterface $eventDate = null;
}
2.5 集合约束 #
php
<?php
class Order
{
#[Assert\NotBlank]
#[Assert\Count(
min: 1,
max: 10,
minMessage: '至少选择一个商品',
maxMessage: '最多选择{{ limit }}个商品'
)]
private array $items = [];
#[Assert\All([
new Assert\NotBlank(),
new Assert\Email(),
])]
private array $emails = [];
#[Assert\Unique(message: '标签不能重复')]
private array $tags = [];
}
三、表单约束 #
3.1 表单字段约束 #
php
<?php
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\Image;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'constraints' => [
new Assert\NotBlank(['message' => '请输入姓名']),
new Assert\Length(['min' => 2]),
],
])
->add('avatar', FileType::class, [
'mapped' => false,
'constraints' => [
new File([
'maxSize' => '2M',
'mimeTypes' => ['image/jpeg', 'image/png'],
'mimeTypesMessage' => '请上传有效的图片文件',
]),
new Image([
'minWidth' => 100,
'maxWidth' => 2000,
'minHeight' => 100,
'maxHeight' => 2000,
]),
],
])
;
}
}
3.2 回调约束 #
php
<?php
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class User
{
#[Assert\Callback]
public function validate(ExecutionContextInterface $context): void
{
if ($this->age < 18 && $this->hasParentConsent === false) {
$context->buildViolation('未成年人需要家长同意')
->atPath('hasParentConsent')
->addViolation();
}
}
}
3.3 类级别约束 #
php
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[Assert\Callback('validateDates')]
class Event
{
private ?\DateTimeInterface $startDate = null;
private ?\DateTimeInterface $endDate = null;
public static function validateDates(self $event, ExecutionContextInterface $context): void
{
if ($event->startDate && $event->endDate) {
if ($event->startDate >= $event->endDate) {
$context->buildViolation('结束日期必须晚于开始日期')
->atPath('endDate')
->addViolation();
}
}
}
}
四、自定义约束 #
4.1 创建约束类 #
php
<?php
namespace App\Validator\Constraints;
use Attribute;
use Symfony\Component\Validator\Constraint;
#[Attribute]
class ContainsAlphanumeric extends Constraint
{
public string $message = '字符串"{{ string }}"包含非法字符,只能包含字母和数字。';
public function validatedBy(): string
{
return static::class . 'Validator';
}
}
4.2 创建验证器 #
php
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ContainsAlphanumericValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof ContainsAlphanumeric) {
throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
}
if (null === $value || '' === $value) {
return;
}
if (!is_string($value)) {
throw new UnexpectedValueException($value, 'string');
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ string }}', $value)
->addViolation();
}
}
}
4.3 使用自定义约束 #
php
<?php
use App\Validator\Constraints\ContainsAlphanumeric;
class User
{
#[ContainsAlphanumeric(message: '用户名只能包含字母和数字')]
private ?string $username = null;
}
五、验证组 #
5.1 定义验证组 #
php
<?php
class User
{
#[Assert\NotBlank(groups: ['registration', 'profile'])]
private ?string $name = null;
#[Assert\NotBlank(groups: ['registration'])]
#[Assert\Email(groups: ['registration', 'profile'])]
private ?string $email = null;
#[Assert\NotBlank(groups: ['registration'])]
#[Assert\Length(min: 8, groups: ['registration'])]
private ?string $password = null;
#[Assert\NotBlank(groups: ['profile'])]
private ?string $bio = null;
}
5.2 使用验证组 #
php
<?php
public function register(Request $request): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user, [
'validation_groups' => ['registration'],
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 处理注册
}
}
public function profile(Request $request): Response
{
$user = $this->getUser();
$form = $this->createForm(UserType::class, $user, [
'validation_groups' => ['profile'],
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 处理资料更新
}
}
5.3 动态验证组 #
php
<?php
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form) {
$user = $form->getData();
if ($user->getId()) {
return ['profile'];
}
return ['registration'];
},
]);
}
}
六、错误处理 #
6.1 获取表单错误 #
php
<?php
public function submit(Request $request): Response
{
$form = $this->createForm(UserType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && !$form->isValid()) {
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[$error->getOrigin()->getName()] = $error->getMessage();
}
return $this->json(['errors' => $errors], 400);
}
}
6.2 模板中显示错误 #
twig
{# 显示全局错误 #}
{% if form.vars.errors|length > 0 %}
<div class="alert alert-danger">
{% for error in form.vars.errors %}
<p>{{ error.message }}</p>
{% endfor %}
</div>
{% endif %}
{# 显示字段错误 #}
<div class="form-group">
{{ form_label(form.email) }}
{{ form_widget(form.email) }}
{% if form.email.vars.errors|length > 0 %}
<div class="invalid-feedback">
{% for error in form.email.vars.errors %}
{{ error.message }}
{% endfor %}
</div>
{% endif %}
</div>
6.3 自定义错误消息 #
php
<?php
#[Assert\NotBlank(message: '用户名不能为空')]
private ?string $username = null;
#[Assert\Email(
message: '{{ value }} 不是有效的邮箱地址',
mode: 'strict'
)]
private ?string $email = null;
#[Assert\Length(
min: 8,
max: 50,
minMessage: '密码至少需要{{ limit }}个字符',
maxMessage: '密码最多{{ limit }}个字符',
exactMessage: '密码必须正好{{ limit }}个字符'
)]
private ?string $password = null;
七、手动验证 #
7.1 验证实体 #
php
<?php
use Symfony\Component\Validator\Validator\ValidatorInterface;
class UserService
{
public function __construct(
private ValidatorInterface $validator
) {}
public function validate(User $user): array
{
$errors = $this->validator->validate($user);
$messages = [];
foreach ($errors as $error) {
$messages[$error->getPropertyPath()] = $error->getMessage();
}
return $messages;
}
public function isValid(User $user): bool
{
return count($this->validator->validate($user)) === 0;
}
public function validateGroup(User $user, string $group): array
{
$errors = $this->validator->validate($user, null, [$group]);
return $errors;
}
}
八、总结 #
本章学习了:
- 验证组件概述
- 实体约束定义
- 表单字段约束
- 自定义约束
- 验证组
- 错误处理
- 手动验证
下一章将学习 服务基础。
最后更新:2026-03-28