MidТеория5 min

Constraints валидации

Встроенные constraints: NotBlank, Email, Range, Type, Length, группы валидации, nullable-поля

Компонент 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, '', [] -- строже, чем NotNull
  • NotNull отклоняет только null, пустая строка проходит
  • Большинство constraints пропускают null (кроме NotBlank/NotNull)
  • propertyPath позволяет сравнивать с другим свойством объекта
  • #[Assert\Valid] необходим для каскадной валидации вложенных объектов
  • Constraint можно ставить на свойства, getter-методы и сам класс
  • Шаблонные переменные: {{ value }}, {{ limit }}, {{ min }}, {{ max }}

Проверь себя

Как с помощью constraint сравнить значение одного свойства с другим?

Что произойдёт, если nullable-поле с `#[Assert\Email]` содержит null?

Что означает `{{ limit }}` в сообщении `minMessage` constraint `#[Assert\Length]`?

Для чего нужен `#[Assert\Valid]` на вложенном объекте?

Какая разница между `#[Assert\NotBlank]` и `#[Assert\NotNull]`?