Компонент Validator
Symfony Validator -- компонент для проверки данных на соответствие правилам (constraints). Валидация выполняется автоматически при обработке форм или вручную через сервис ValidatorInterface.
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class User
{
public function __construct(
#[Assert\NotBlank(message: 'Name is required')]
#[Assert\Length(min: 2, max: 100)]
private string $name,
#[Assert\NotBlank]
#[Assert\Email(message: 'Invalid email "{{ value }}"')]
private string $email,
#[Assert\Range(min: 18, max: 150, notInRangeMessage: 'Age must be between {{ min }} and {{ max }}')]
private int $age,
) {
}
// Getters...
}
Валидация через сервис
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class UserService
{
public function __construct(
private readonly ValidatorInterface $validator,
) {
}
public function createUser(string $name, string $email, int $age): User
{
$user = new User($name, $email, $age);
$violations = $this->validator->validate($user);
if (count($violations) > 0) {
// ConstraintViolationListInterface
foreach ($violations as $violation) {
// $violation->getPropertyPath() -- 'name', 'email', etc.
// $violation->getMessage() -- 'Name is required'
// $violation->getInvalidValue() -- actual invalid value
}
throw new ValidationException($violations);
}
return $user;
}
}
Основные встроенные Constraints
Строковые constraints
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class Article
{
// NotBlank -- value must not be empty string, null, or empty array
#[Assert\NotBlank]
private string $title;
// NotNull -- value must not be null (empty string IS valid)
#[Assert\NotNull]
private ?string $subtitle;
// Length -- string length constraints
#[Assert\Length(
min: 10,
max: 5000,
minMessage: 'Content must be at least {{ limit }} characters',
maxMessage: 'Content cannot exceed {{ limit }} characters',
)]
private string $content;
// Regex -- pattern matching
#[Assert\Regex(
pattern: '/^[a-z0-9-]+$/',
message: 'Slug may only contain lowercase letters, numbers, and hyphens',
)]
private string $slug;
// Url
#[Assert\Url(protocols: ['http', 'https'])]
private ?string $website = null;
}
Подвох экзамена:
#[Assert\NotBlank]отклоняетnull, пустую строку''и пустой массив[].#[Assert\NotNull]отклоняет ТОЛЬКОnull. Пустая строка''проходитNotNull, но неNotBlank. Это частый вопрос на экзамене.
Числовые constraints
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class Product
{
// Range -- min/max for numbers
#[Assert\Range(min: 0, max: 999999)]
private int $price;
// Positive -- must be > 0
#[Assert\Positive]
private int $quantity;
// PositiveOrZero -- must be >= 0
#[Assert\PositiveOrZero]
private int $stock;
// LessThan / GreaterThan
#[Assert\GreaterThan(0)]
private float $weight;
#[Assert\LessThanOrEqual(100)]
private int $discount;
// DivisibleBy
#[Assert\DivisibleBy(value: 5, message: 'Quantity must be a multiple of 5')]
private int $bulkQuantity;
}
Email constraint
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
// Basic email validation
#[Assert\Email]
private string $email;
// Strict email validation (checks DNS MX record)
#[Assert\Email(mode: Assert\Email::VALIDATION_MODE_STRICT)]
private string $strictEmail;
// HTML5 mode (browser-compatible pattern)
#[Assert\Email(mode: Assert\Email::VALIDATION_MODE_HTML5)]
private string $html5Email;
Type constraint
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
// Validate PHP type
#[Assert\Type('int')]
private mixed $count;
#[Assert\Type('numeric')] // int, float, or numeric string
private mixed $amount;
#[Assert\Type(\DateTimeInterface::class)]
private mixed $date;
#[Assert\Type('bool')]
private mixed $active;
Сравнительные constraints
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
final class Event
{
#[Assert\NotBlank]
private \DateTimeImmutable $startDate;
// Compare with another property
#[Assert\GreaterThan(propertyPath: 'startDate', message: 'End date must be after start date')]
private \DateTimeImmutable $endDate;
// Compare with a value
#[Assert\GreaterThanOrEqual('today', message: 'Start date must be in the future')]
private \DateTimeImmutable $registrationDeadline;
}
Подвох экзамена:
propertyPathпозволяет сравнивать значение с другим свойством ТОГО ЖЕ объекта. Это мощный механизм для кросс-полевой валидации. Строка'today'автоматически конвертируется вDateTimeImmutable.
Choice constraint
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\Choice(choices: ['draft', 'published', 'archived'])]
private string $status;
// With callback -- dynamic choices
#[Assert\Choice(callback: [self::class, 'getAvailableStatuses'])]
private string $dynamicStatus;
// Multiple values
#[Assert\Choice(
choices: ['php', 'javascript', 'python', 'go'],
multiple: true,
min: 1,
max: 3,
multipleMessage: 'Select at least {{ min }} skills',
)]
/** @var list<string> */
private array $skills;
public static function getAvailableStatuses(): array
{
return ['draft', 'review', 'published'];
}
Collection и структурные constraints
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
// Count -- validate array/collection size
#[Assert\Count(min: 1, max: 10, minMessage: 'At least one tag is required')]
/** @var list<string> */
private array $tags;
// All -- validate each element
#[Assert\All([
new Assert\NotBlank(),
new Assert\Length(max: 50),
])]
/** @var list<string> */
private array $names;
// Unique -- all values must be unique
#[Assert\Unique(message: 'Tags must be unique')]
/** @var list<string> */
private array $uniqueTags;
Nullable поля и валидация
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
final class Profile
{
// Nullable field: if provided, must be valid URL
#[Assert\Url]
private ?string $website = null;
// null is valid! URL constraint is only checked if value is not null
// REQUIRE value AND validate format
#[Assert\NotNull]
#[Assert\Email]
private ?string $email;
// null is INVALID (NotNull), and non-null must be valid email
// NotBlank covers both null and empty string
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
private string $name;
}
Подвох экзамена: Большинство constraints (Email, Url, Length, Range и др.) автоматически пропускают
nullзначения. Если поле nullable и значение null, constraint считается пройденным. Чтобы запретить null, добавьте#[Assert\NotNull]или#[Assert\NotBlank].
Constraint на уровне свойства vs метода
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class Order
{
#[Assert\NotBlank]
private string $customerName;
private float $subtotal;
private float $tax;
// Validate getter return value
#[Assert\Positive(message: 'Total must be positive')]
public function getTotal(): float
{
return $this->subtotal + $this->tax;
}
// IsTrue/IsFalse on method
#[Assert\IsTrue(message: 'Order must have at least one item')]
public function hasItems(): bool
{
return count($this->items) > 0;
}
}
Constraint с параметрами из переменных шаблона
<?php
declare(strict_types=1);
use Symfony\Component\Validator\Constraints as Assert;
// {{ value }} -- submitted value
// {{ limit }} -- constraint limit (Length)
// {{ min }}, {{ max }} -- Range limits
#[Assert\Length(
min: 8,
max: 128,
minMessage: 'Password "{{ value }}" is too short. Minimum {{ limit }} characters.',
maxMessage: 'Password must not exceed {{ limit }} characters.',
)]
private string $password;
#[Assert\Range(
min: 1,
max: 100,
notInRangeMessage: 'Value {{ value }} is not in range {{ min }}-{{ max }}.',
)]
private int $percentage;
Встроенная валидация вложенных объектов
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class Order
{
#[Assert\NotBlank]
private string $number;
// Validate nested object recursively
#[Assert\Valid]
private Address $shippingAddress;
// Validate each item in collection
#[Assert\Valid]
#[Assert\Count(min: 1)]
/** @var list<OrderItem> */
private array $items = [];
}
Подвох экзамена: Без
#[Assert\Valid]вложенные объекты НЕ валидируются. Symfony не выполняет каскадную валидацию автоматически. Каждый вложенный объект, который нужно провалидировать, должен быть помечен#[Assert\Valid].
Итоги
NotBlankотклоняет null, '', [] -- строже, чемNotNullNotNullотклоняет только null, пустая строка проходит- Большинство constraints пропускают null (кроме NotBlank/NotNull)
propertyPathпозволяет сравнивать с другим свойством объекта#[Assert\Valid]необходим для каскадной валидации вложенных объектов- Constraint можно ставить на свойства, getter-методы и сам класс
- Шаблонные переменные:
{{ value }},{{ limit }},{{ min }},{{ max }}