Symfony/Validator для PHP: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Symfony/Validator для PHP: полное руководство с примерами

1. Что это и зачем нужна

Библиотека Symfony/Validator — это мощный и гибкий компонент экосистемы Symfony, предназначенный для валидации данных. В отличие от простых проверок "на лету", этот компонент предлагает декларативный подход: вы описываете правила валидации (ограничения) с помощью аннотаций, атрибутов PHP 8+, YAML, XML или PHP-массивов, а затем передаёте данные специальному сервису — ValidatorInterface. Это позволяет отделить логику проверки от бизнес-логики приложения, делая код более чистым, тестируемым и переиспользуемым.

Зачем это нужно? В любом веб-приложении (и не только) данные поступают из ненадёжных источников: формы на сайте, API-запросы, импорт из CSV-файлов. Без валидации вы рискуете получить SQL-инъекции, XSS-атаки, некорректные расчёты или просто "мусор" в базе данных. Symfony/Validator решает эти проблемы комплексно: он проверяет типы данных, длины строк, диапазоны чисел, уникальность email, соответствие регулярным выражениям и многое другое. Более того, он умеет собирать все ошибки в единый объект ConstraintViolationList, который можно легко преобразовать в JSON-ответ для API или отобразить в виде списка ошибок на форме.

Компонент полностью независим от фреймворка Symfony. Вы можете использовать его в любом PHP-проекте (Laravel, Yii, собственный фреймворк или даже "чистый" PHP). Он активно поддерживается, имеет отличную документацию и огромное сообщество. Это "золотой стандарт" валидации в мире PHP.

2. Установка

Установка производится через Composer. Выполните в терминале в корне вашего проекта:

composer require symfony/validator

Также рекомендуется установить компонент symfony/property-access для корректной работы с вложенными объектами и массивами:

composer require symfony/property-access

Если вы планируете использовать аннотации/атрибуты, убедитесь, что у вас установлен doctrine/annotations (для PHP 7.x) или вы используете PHP 8+ с нативными атрибутами.

3. Быстрый старт — минимальный рабочий пример

Создадим простой класс User с атрибутами валидации и проверим его.

<?php
// 1. Подключаем автозагрузку Composer
require_once 'vendor/autoload.php';

// 2. Импортируем необходимые классы
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;

// 3. Создаём класс с атрибутами валидации (PHP 8+)
class User {
    #[Assert\NotBlank(message: "Имя не может быть пустым")]
    #[Assert\Length(min: 2, max: 50, minMessage: "Имя должно содержать минимум {{ limit }} символа")]
    public string $name;

    #[Assert\NotBlank]
    #[Assert\Email(message: "Email '{{ value }}' не является валидным.")]
    public string $email;

    #[Assert\PositiveOrZero]
    public int $age;
}

// 4. Создаём валидатор
$validator = Validation::createValidatorBuilder()
    ->enableAttributeMapping() // Включаем поддержку атрибутов
    ->getValidator();

// 5. Создаём объект с некорректными данными
$user = new User();
$user->name = 'A'; // Слишком короткое имя
$user->email = 'not-an-email';
$user->age = -5;

// 6. Валидируем
$violations = $validator->validate($user);

// 7. Обрабатываем ошибки
if (count($violations) > 0) {
    foreach ($violations as $violation) {
        echo $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
    }
} else {
    echo "Данные валидны!";
}

Этот пример выведет:

name: Имя должно содержать минимум 2 символа
email: Email 'not-an-email' не является валидным.
age: Это значение должно быть положительным или равным нулю.

4. Основные методы/функции/классы

4.1. Validation::createValidatorBuilder()

Сигнатура: public static function createValidatorBuilder(): ValidatorBuilder

Что делает: Создаёт и возвращает строитель (builder) для конфигурации валидатора. Это отправная точка для настройки всех параметров.

$builder = Validation::createValidatorBuilder();
$validator = $builder->getValidator(); // Получаем готовый валидатор

4.2. ValidatorBuilder::getValidator()

Сигнатура: public function getValidator(): ValidatorInterface

Что делает: Собирает и возвращает готовый к использованию объект валидатора на основе настроек, переданных в builder.

$validator = Validation::createValidatorBuilder()
    ->enableAttributeMapping()
    ->getValidator();

4.3. ValidatorInterface::validate()

Сигнатура: public function validate(mixed $value, Constraint|array|null $constraints = null, string|GroupSequence|array|null $groups = null): ConstraintViolationListInterface

Что делает: Основной метод для валидации. Принимает значение (объект, массив, скаляр) и возвращает список нарушений (ConstraintViolationListInterface). Если нарушений нет, список будет пустым.

$user = new User();
$user->email = 'test@example.com';
$violations = $validator->validate($user);

if (0 === count($violations)) {
    echo "OK";
}

4.4. ConstraintViolationListInterface

Сигнатура: Это интерфейс, реализуемый классом ConstraintViolationList. Основные методы: count(), get(int $offset), getIterator().

Что делает: Представляет собой коллекцию объектов ConstraintViolationInterface. Позволяет итерироваться по ошибкам и получать их количество.

$violations = $validator->validate($data);
echo "Найдено ошибок: " . count($violations);
foreach ($violations as $violation) {
    // $violation - объект ConstraintViolationInterface
}

4.5. ConstraintViolationInterface::getMessage()

Сигнатура: public function getMessage(): string

Что делает: Возвращает сообщение об ошибке, которое было задано в ограничении (или стандартное сообщение).

foreach ($violations as $violation) {
    echo $violation->getMessage(); // "Это значение не должно быть пустым."
}

4.6. ConstraintViolationInterface::getPropertyPath()

Сигнатура: public function getPropertyPath(): string

Что делает: Возвращает путь к свойству, которое вызвало ошибку. Для вложенных объектов путь может быть составным (например, address.city).

foreach ($violations as $violation) {
    echo $violation->getPropertyPath(); // "email" или "address.street"
}

4.7. ConstraintViolationInterface::getInvalidValue()

Сигнатура: public function getInvalidValue(): mixed

Что делает: Возвращает то самое некорректное значение, которое было передано и не прошло валидацию. Полезно для логирования или отладки.

foreach ($violations as $violation) {
    $badValue = $violation->getInvalidValue();
    error_log("Некорректное значение: " . print_r($badValue, true));
}

4.8. ValidatorBuilder::enableAnnotationMapping() / enableAttributeMapping()

Сигнатура: public function enableAnnotationMapping(): static (и аналогично для атрибутов)

Что делает: Включает чтение ограничений из аннотаций (Doctrine) или нативных атрибутов PHP 8. Без этого вызова валидатор не увидит атрибуты в ваших классах.

$validator = Validation::createValidatorBuilder()
    ->enableAttributeMapping() // Для PHP 8+
    ->getValidator();

4.9. Validation::createValidator()

Сигнатура: public static function createValidator(): ValidatorInterface

Что делает: Быстрый способ получить валидатор "из коробки" без какой-либо дополнительной конфигурации. Подходит для простых случаев, когда не нужно загружать метаданные из аннотаций.

$validator = Validation::createValidator();
$violations = $validator->validate('some_value', new Assert\Email());

4.10. ValidatorInterface::validateProperty()

Сигнатура: public function validateProperty(object $object, string $propertyName, string|GroupSequence|array|null $groups = null): ConstraintViolationListInterface

Что делает: Позволяет валидировать только одно конкретное свойство объекта. Это эффективнее, чем валидировать весь объект, если вы знаете, какое поле изменилось (например, при валидации на стороне клиента через AJAX).

$user = new User();
$user->email = 'invalid';
$violations = $validator->validateProperty($user, 'email');
// Будут проверены только ограничения для свойства $email

5. Реальный практический пример

Представьте, что мы создаём REST API для регистрации пользователей. Нам нужно провалидировать входящий JSON-запрос. В этом примере мы покажем, как использовать группы валидации и кастомные ограничения.

<?php
require_once 'vendor/autoload.php';

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

// --- Шаг 1: Создаём кастомное ограничение для проверки сложности пароля ---

class StrongPassword extends Constraint {
    public string $message = 'Пароль должен содержать минимум 8 символов, одну заглавную букву и одну цифру.';
}

class StrongPasswordValidator extends ConstraintValidator {
    public function validate(mixed $value, Constraint $constraint): void {
        if (!preg_match('/^(?=.*[A-Z])(?=.*\d).{8,}$/', $value)) {
            $this->context->buildViolation($constraint->message)
                ->addViolation();
        }
    }
}

// --- Шаг 2: Создаём DTO (Data Transfer Object) с группами валидации ---

class RegistrationRequest {
    #[Assert\NotBlank(groups: ['registration'])]
    #[Assert\Length(min: 2, max: 50, groups: ['registration'])]
    public string $username;

    #[Assert\NotBlank(groups: ['registration'])]
    #[Assert\Email(groups: ['registration'])]
    public string $email;

    #[Assert\NotBlank(groups: ['registration'])]
    #[StrongPassword(groups: ['registration'])]
    public string $password;

    #[Assert\NotBlank(groups: ['registration'])]
    #[Assert\EqualTo(propertyPath: 'password', message: 'Пароли не совпадают', groups: ['registration'])]
    public string $confirmPassword;

    #[Assert\Type(type: 'bool', groups: ['optional'])]
    public bool $subscribeToNewsletter = false;
}

// --- Шаг 3: Настраиваем валидатор и регистрируем кастомный валидатор ---

$validator = Validation::createValidatorBuilder()
    ->enableAttributeMapping()
    ->addMethodMapping(function (ExecutionContextInterface $context) {
        // Можно добавить кастомную логику на уровне класса
    })
    ->getValidator();

// --- Шаг 4: Имитируем входящий JSON-запрос ---

$jsonPayload = '{
    "username": "jo",
    "email": "bad-email",
    "password": "weak",
    "confirmPassword": "weak1"
}';

$data = json_decode($jsonPayload, false); // Преобразуем в объект

// Создаём объект запроса и заполняем его
$request = new RegistrationRequest();
$request->username = $data->username ?? '';
$request->email = $data->email ?? '';
$request->password = $data->password ?? '';
$request->confirmPassword = $data->confirmPassword ?? '';

// --- Шаг 5: Валидируем с использованием группы 'registration' ---

$violations = $validator->validate($request, null, ['registration']);

// --- Шаг 6: Формируем ответ для API ---

if (count($violations) > 0) {
    $errors = [];
    foreach ($violations as $violation) {
        $errors[] = [
            'field' => $violation->getPropertyPath(),
            'message' => $violation->getMessage(),
        ];
    }

    http_response_code(422);
    echo json_encode(['status' => 'error', 'errors' => $errors], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} else {
    http_response_code(201);
    echo json_encode(['status' => 'success', 'message' => 'Пользователь зарегистрирован!'], JSON_UNESCAPED_UNICODE);
}

// --- Ожидаемый вывод (из-за некорректных данных) ---
// {
//     "status": "error",
//     "errors": [
//         {
//             "field": "username",
//             "message": "Имя пользователя должно содержать минимум 2 символа"
//         },
//         {
//             "field": "email",
//             "message": "Значение \"bad-email\" не является валидным адресом электронной почты."
//         },
//         {
//             "field": "password",
//             "message": "Пароль должен                    

Рекомендаций