Symfony Validator: проверка данных в PHP — мастерская и 6 примеров
Проверяем формы и JSON с Symfony Validator без фреймворка: шесть живых сценариев, мини-мастерская ограничений и готовый PHP-проект. Разбираем ошибки полей, вложенные данные, даты, импорт и подключение через Composer.

Форма отправляет пустое имя, JSON содержит строку вместо числа, дата окончания оказывается раньше начала, а в десятой строке импорта неверный email. Во всех случаях хочется получить одинаково понятный результат: какое поле не прошло проверку, почему и что исправить.
Symfony Validator — отдельный PHP-компонент для описания таких правил. Его можно установить через Composer в обычный проект, без полного фреймворка Symfony. В этой статье есть мини-мастерская и шесть работающих примеров: контактная форма, регистрация, расчёт стоимости, период работ, вложенный заказ и импорт строк.
Ручные if и filter_var или библиотека правил
Проверить единственное поле можно несколькими условиями. Это нормальный выбор для маленького скрипта. Сложность появляется, когда те же требования нужны в форме, API, импорте и консольной команде: проверки расходятся, сообщения дублируются, а вложенные ошибки приходится собирать вручную.
$email = $input['email'] ?? null;
$errors = [];
if (!is_string($email) || trim($email) === '') {
$errors['email'][] = 'Введите email';
} elseif (filter_var(trim($email), FILTER_VALIDATE_EMAIL) === false) {
$errors['email'][] = 'Проверьте формат email';
}
В Validator условие оформляется как constraint — ограничение, например NotBlank, Email или Range. Проверяющий механизм возвращает список нарушений с путями полей. Правила можно переиспользовать для массивов и объектов, сочетать в цепочки и дополнять проверками связанных значений.
| Задача | Ручная проверка | Symfony Validator |
|---|---|---|
| Одно простое условие | Короткий if обычно достаточен | Дополнительная зависимость может не понадобиться |
| Одинаковые правила в нескольких местах | Нужно самостоятельно вынести и поддерживать функции | Общие ограничения, DTO и группы проверки |
| Вложенные строки заказа | Индексы и ошибки собираются вручную | Collection, All и пути нарушений |
| Сохранение и права | Реализуются приложением | Также остаются задачей приложения |
HTML-атрибуты required, min и браузерная проверка улучшают ввод, но запрос можно отправить напрямую. Сервер проверяет данные независимо от интерфейса. При этом успешная валидация означает соответствие заданным правилам, а не разрешение операции, существование пользователя или готовность данных к любому способу вывода.
Установка через Composer и первый работающий скрипт
В примерах закреплён Symfony Validator 7.4.23 из поддерживаемой ветки 7.4 LTS. Требуется PHP 8.2 или новее. Установка, autoload и разница между install и update подробно разобраны в руководстве по Composer.
mkdir validator-demo
cd validator-demo
composer require symfony/validator:7.4.23
Сохраните следующий полный пример как check.php рядом с папкой vendor. Здесь ещё нет веб-сервера, формы и базы: проверяем значение из командной строки и выводим сообщения.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
$validator = Validation::createValidator();
$violations = $validator->validate('wrong-address', new Assert\Sequentially([
new Assert\Type('string'),
new Assert\NotBlank(message: 'Введите email', normalizer: 'trim'),
new Assert\Email(message: 'Проверьте формат email', normalizer: 'trim'),
]));
foreach ($violations as $violation) {
echo $violation->getMessage(), PHP_EOL;
}
echo count($violations) === 0 ? 'Данные прошли проверку' : 'Есть ошибки';
php check.php
composer show symfony/validator
composer check-platform-reqs
Ожидаемая ошибка ввода возвращается в ConstraintViolationList. Для неё не нужен catch вокруг каждого поля. Исключение из-за неверно настроенного ограничения или ошибки PHP — другая ситуация: его нельзя превращать в сообщение «данные корректны» или выдавать посетителю вместе со стеком вызовов.
Версия и требования: официальная страница Symfony 7.4 LTS.
Точная версия в команде помогает повторить материал с тем же поведением. В рабочем проекте обновления зависимостей планируют отдельно: Composer проверяет совместимость, а ваши проверки подтверждают, что сообщения, границы и API остаются ожидаемыми. Передавайте вместе с проектом composer.json и composer.lock; папка vendor восстанавливается командой install.
Мини-мастерская: одно поле, правила и настоящий ответ сервера
Выберите тип поля: строка, email, число, дата или значение из списка. Настройте обязательность и доступные ограничения, введите значение и запустите проверку. Браузер передаёт конфигурацию обработчику; разрешённые правила применяет Symfony Validator на PHP.
Попробуйте: сделайте email обязательным и отправьте пустое значение. Затем введите неправильный адрес и исправьте его на demo@example.test. Сравните сообщение об обязательности с ошибкой формата. Отключите обязательность и снова проверьте пустоту: необязательное поле может отсутствовать, но заполненное всё равно должно соответствовать своему типу.
Мастерская возвращает PHP-код для выбранных правил и JSON настроек. После изменения поля или ограничений старый ответ перестаёт описывать текущий ввод: сначала выполните новую проверку. Генератор использует ограниченный набор готовых правил, а не исполняет произвольный PHP, присланный посетителем.
Экспортированный PHP запускается с Composer-зависимостями. Обычный HTML-файл не умеет выполнить серверную проверку сам: для полноценного повторения скачайте проект и запустите локальный PHP-сервер по инструкции в конце статьи.
Для следующего опыта выберите число и установите нижнюю границу. Сравните пустое поле, ноль и значение ровно на границе. Затем переключитесь на список: здесь проверяется принадлежность разрешённым вариантам, а не длина текста. Такой конструктор полезен при проектировании формы: сначала договоритесь о данных и пограничных случаях, потом переносите правила в приложение.
Шесть примеров: от поля формы до вложенного JSON
В каждом сценарии есть понятные поля, готовые данные и результат реального запроса. Начните с корректного набора, внесите одну ошибку, затем исправьте её. Так видно, какое ограничение сработало и как путь нарушения связывается с конкретным полем.
Проверка не означает отправку заявки или регистрацию. Сервер только разбирает входной набор, применяет правила и возвращает ответ. Если соединение пропало, интерфейс должен сообщить о недоступной проверке, а не показывать старый зелёный результат.
Скачать полный проект с PHP-ядром, интерфейсами и примерами. В архиве используется тот же подход к проверке, что и на странице; доступны PHP-сниппеты и JSON с данными.
1. Контактная форма: обязательность, email и согласие
Проверяем имя от 2 до 80 символов, email длиной до 180, сообщение от 10 до 1000 и обязательное согласие. Пустой адрес и адрес неправильного формата — разные ошибки. Полезно сначала объяснить отсутствие значения, а уже потом разбирать его содержание.
Попробуйте: очистите имя, введите wrong-address и снимите согласие. После запроса найдите ошибки у соответствующих полей. Верните подготовленный корректный набор: результат должен сообщить об успешной проверке, а не об отправленном письме.
$contactRules = new Assert\Collection([
'name' => new Assert\Sequentially([
new Assert\Type('string'),
new Assert\NotBlank(normalizer: 'trim'),
new Assert\Length(min: 2, max: 80, normalizer: 'trim'),
]),
'email' => new Assert\Sequentially([
new Assert\Type('string'),
new Assert\NotBlank(normalizer: 'trim'),
new Assert\Length(max: 180, normalizer: 'trim'),
new Assert\Email(normalizer: 'trim'),
]),
'message' => new Assert\Sequentially([
new Assert\Type('string'), new Assert\NotBlank(normalizer: 'trim'),
new Assert\Length(min: 10, max: 1000, normalizer: 'trim'),
]),
'consent' => new Assert\Sequentially([
new Assert\NotNull(), new Assert\Type('bool'), new Assert\IsTrue(),
]),
]);
$violations = $validator->validate($input, $contactRules);
IsTrue сам допускает также число 1 и строку "1". В JSON-контракте примера требуется именно boolean, поэтому перед ним стоят NotNull и Type('bool'). Библиотека проверяет правила; сбор данных формы, текст согласия и последующая обработка обращения принадлежат приложению.
Документация: точное поведение IsTrue.
Это основа формы обратной связи, заявки на расчёт или обращения в поддержку. В готовом наборе указаны Анна и адрес anna@example.test; домен используется как пример. Успешный Email подтверждает формат, но не доставку письма и не владение ящиком. Подтверждение адреса отдельной ссылкой, защита от повторных отправок и отправка уведомления требуют собственной логики.
Документация: Email: формат, пустые значения и режимы проверки.
2. Регистрация: ограничения полей и совпадение паролей
В демонстрации логин содержит 3–24 латинские буквы, цифры, подчёркивание или дефис. Пароль — от 8 до 64 символов, подтверждение должно совпадать, условия должны быть приняты. Используйте учебные значения: результат проверки не создаёт аккаунт.
Попробуйте: замените подтверждение пароля отличающимся текстом. Ошибка должна относиться к confirmPassword, чтобы человек сразу понимал, что исправлять. Затем задайте одинаковые значения. Пробелы пароля не удаляются автоматически: это часть введённого секрета, а не обычное оформление имени.
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class PasswordPair
{
public mixed $password = null;
public mixed $confirmPassword = null;
#[Assert\Callback]
public function checkMatch(ExecutionContextInterface $context): void
{
if (!is_string($this->password) || !is_string($this->confirmPassword)) {
return; // Ошибки типов проверяются отдельными ограничениями.
}
if ($this->password !== $this->confirmPassword) {
$context->buildViolation('Пароли не совпадают')
->atPath('confirmPassword')
->addViolation();
}
}
}
Этот фрагмент показывает только межполевую проверку. Полный сценарий дополнительно проверяет тип, длину и обязательность каждого значения. Callback добавляет нарушение через контекст; return false не является способом сообщить ошибку. Пароль и его подтверждение не нужно возвращать в JSON ошибок или записывать в журнал.
Документация: Callback и привязка нарушения к полю.
Такая проверка нужна и при смене пароля, и при приглашении сотрудника во внутренний сервис. Ограничения длины в демонстрации показывают механизм; политика паролей конкретного продукта может отличаться. Проверка уникальности логина потребует обращения к хранилищу, а создание учётной записи — отдельной операции. Здесь посетитель получает только проверку набора, поэтому результат не обещает, что логин свободен.
Открыть проверку регистрации ↑
3. Расчёт стоимости: числа, диапазоны и пустые значения
У позиции есть название, количество, цена за единицу и скидка. Количество должно быть больше нуля и не превышать 10 000; цена — от 0 до 1 000 000; скидка — от 0 до 50%. Для этого примера допускается не больше двух знаков после десятичной точки.
Попробуйте: задайте количество 0, затем отрицательную цену и скидку 60. Исправьте их по очереди. Итог следует считать только после проверки всего набора: число в карточке не должно выглядеть достоверным, если одна из его составляющих недопустима.
$quantityRules = new Assert\Sequentially([
new Assert\NotNull(message: 'Укажите количество'),
new Assert\Type(type: ['int', 'float'], message: 'Количество должно быть числом'),
new Assert\GreaterThan(value: 0, message: 'Количество должно быть больше нуля'),
new Assert\LessThanOrEqual(value: 10000, message: 'Максимум — 10000'),
]);
// Проверка количества знаков после точки — отдельное правило примера.
$violations = $validator->validate($input['quantity'] ?? null, $quantityRules);
JSON-число 2 и строка "2" различаются. Наш обработчик не превращает строку в число; форма выполняет явное преобразование корректного числового ввода, а пустоту передаёт как null. Это защищает от распространённой ошибки, когда пустое поле незаметно становится нулём.
function readNumber(input) {
const raw = input.value.trim();
if (raw === '') return null;
const value = Number(raw);
if (!Number.isFinite(value)) throw new Error('Введите конечное число');
return value;
}
Validator проверяет диапазон и тип, но не определяет финансовую модель. Округление, допустимая точность и работа с копейками должны быть согласованы отдельно. Нулевая цена может означать бесплатную позицию; нулевое количество в этом сценарии запрещено. Одинаковая проверка «не пусто» для обоих полей не описывает такую разницу.
Контрольный набор: количество 2, цена 1500, скидка 10%. После успешной проверки получается 2700. Сценарий можно использовать для строки коммерческого предложения, калькулятора услуги или редактора заказа. В этих интерфейсах важно хранить отдельно введённые значения, результат проверки и рассчитанный итог, чтобы ошибка не оставляла на экране сумму от предыдущего набора.
4. Период работ: формат даты и связь двух полей
Корректный формат ещё не делает период корректным. Сначала проверяем календарные даты Y-m-d, затем правило «окончание не раньше начала» и ограничение разницы в 366 суток. Одинаковые даты допустимы: разница равна нулю, дополнительный день к ней не прибавляется.
Попробуйте: поменяйте даты местами. Ошибка должна появиться у окончания. Затем задайте одинаковые даты и проверьте допустимость такого периода. Для календарной формы это понятнее, чем сравнивать случайно выбранные часовые пояса или время полуночи.
function parseIsoDate(mixed $value): ?DateTimeImmutable
{
if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/D', $value)) {
return null;
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
return $date && $date->format('Y-m-d') === $value ? $date : null;
}
$periodRelation = new Assert\Callback(
static function (mixed $data, ExecutionContextInterface $context): void {
if (!is_array($data)) return;
$start = parseIsoDate($data['start'] ?? null);
$end = parseIsoDate($data['end'] ?? null);
if (!$start || !$end) return; // Формат проверяют отдельные Date/NotBlank.
if ($end < $start) {
$context->buildViolation('Окончание не может быть раньше начала')
->atPath('[end]')->addViolation();
} elseif ((int) $start->diff($end)->days > 366) {
$context->buildViolation('Период не должен превышать 366 суток')
->atPath('[end]')->addViolation();
}
}
);
Проверка связи не должна добавлять вторую бессмысленную ошибку поверх неверного формата. Поэтому сначала убеждаемся, что обе даты распознаны. Для бронирования с часами и минутами потребуется отдельный договор о часовом поясе и границах интервала; календарный пример не решает эти вопросы автоматически.
Документация: календарная дата и допустимость пустого значения.
Начальный период 2026-11-06 → 2026-11-16 даёт 10 суток. Это подходит для интервала отчёта, планирования работ или срока действия заявки. Если продукт считает оба календарных дня включительно, формула и объяснение должны измениться вместе. Не заменяйте бизнес-правило случайной арифметикой только потому, что поле уже прошло проверку даты.
5. Вложенный заказ: Collection, All и точный путь ошибки
Заказ содержит email покупателя, способ получения и от одной до десяти позиций. У каждой позиции свой артикул, целое количество от 1 до 99 и цена. Collection описывает поля одного объекта-массива, а All применяет одинаковую проверку к каждому элементу списка.
Попробуйте: испортите количество во второй позиции. В ответе нужен путь именно к этой строке, а не общее «ошибка заказа». Для пути items.1.quantity индекс 1 означает вторую позицию; в подписи интерфейса человеку удобнее видеть номер с единицы.
$itemRules = new Assert\Collection([
'sku' => new Assert\Sequentially([
new Assert\Type('string'), new Assert\NotBlank(),
new Assert\Length(min: 2, max: 24),
]),
'quantity' => new Assert\Sequentially([
new Assert\NotNull(), new Assert\Type('int'),
new Assert\Range(min: 1, max: 99),
]),
'price' => new Assert\Sequentially([
new Assert\NotNull(), new Assert\Type(['int', 'float']),
new Assert\Range(min: 0, max: 1000000),
]),
]);
$itemsRules = new Assert\Sequentially([
new Assert\NotNull(), new Assert\Type('array'),
new Assert\Count(min: 1, max: 10),
new Assert\All([new Assert\Sequentially([
new Assert\NotNull(), new Assert\Type('array'), $itemRules,
])]),
]);
Это фрагмент правил позиций; верхний уровень дополнительно проверяет покупателя и значение pickup либо courier. Наличие ключа и непустое значение различаются: Required требует ключ, а NotBlank проверяет содержимое. Optional разрешает отсутствие поля, но переданное значение всё равно проходит вложенные правила.
$optionalNote = new Assert\Collection([
'title' => new Assert\Required([new Assert\NotBlank()]),
'note' => new Assert\Optional([new Assert\Sequentially([
new Assert\Type('string'), new Assert\Length(max: 200),
])]),
]);
Документация: Collection, Required и Optional.
Документация: проверка каждого элемента через All.
В корректном заказе две строки: KB-01 — две единицы по 1500 и MS-02 — одна по 900. После проверки сводка показывает 3 единицы и 3900. Тот же способ адресации ошибок пригодится для редактора сметы или комплектации оборудования: номер строки сохраняет контекст, даже когда в нескольких позициях одновременно неверно количество.
6. Импорт строк: исправляем ошибки по месту
При пакетном вводе одна неправильная строка не должна теряться среди остальных. В примере проверяются от одной до десяти записей: имя 2–80 символов, email и целый возраст 18–120. Пользователь видит номер строки и поле, а затем исправляет конкретное значение.
Попробуйте: добавьте строку с неправильным email и возрастом 17. После проверки исправьте только одну ошибку и сравните новый ответ. Важно различать количество нарушений и количество ошибочных строк: в одной записи может быть несколько проблем.
$byRow = [];
foreach ($violations as $violation) {
$path = $violation->getPropertyPath();
if (preg_match('/^\[rows\]\[(\d+)\]\[([^]]+)\]$/', $path, $match)) {
$rowNumber = (int) $match[1] + 1;
$byRow[$rowNumber][$match[2]][] = $violation->getMessage();
}
}
Этот фрагмент группирует стандартные пути массива для отчёта. Сам проект отдаёт клиенту согласованный вариант с точками, например rows.1.email. Выберите один формат и используйте его последовательно. В реальном импорте отдельно решается, принимать ли хорошие строки частично или отклонять пакет целиком; текущий пример ничего не записывает.
Корректный набор содержит две записи с возрастом 28 и 35. В наборе с ошибками строковое "35" специально отличается от числа 35: это помогает заметить проблему преобразования CSV или внешнего JSON. При настоящем импорте преобразование выполняют явно до предметной проверки, а не незаметным приведением каждого сомнительного значения к нулю.
Пустота, типы и нормализация: самые важные различия
NotNull запрещает только null. NotBlank также отвергает пустую строку, пустой массив и false, но разрешает 0 и "0". Пробелы не считаются пустотой автоматически. Email сам разрешает null и пустую строку: обязательность задаётся отдельно.
$validator->validate('', new Assert\NotNull()); // 0 нарушений
$validator->validate('', new Assert\NotBlank()); // 1 нарушение
$validator->validate(0, new Assert\NotBlank()); // 0 нарушений
$validator->validate('', new Assert\Email()); // 0 нарушений
$validator->validate(' ', new Assert\NotBlank(normalizer: 'trim')); // 1
Sequentially останавливает цепочку на первой ошибке. Это полезно не только для краткого сообщения: массив не попадёт в строковую проверку, а дорогостоящая проверка не запустится после очевидной ошибки формата. Ограничения описывайте в том порядке, в котором пользователю разумно исправлять ввод.
$rules = new Assert\Sequentially([
new Assert\Type('string'),
new Assert\NotBlank(normalizer: 'trim'),
new Assert\Length(min: 2, max: 80, normalizer: 'trim'),
]);
Normalizer меняет значение только внутри конкретной проверки. Он не переписывает DTO и не передаёт автоматически обрезанную строку следующему ограничению. Если бизнес-логике нужны очищенные данные, создайте отдельную нормализованную копию после проверки типов. Исходные и подготовленные значения не стоит незаметно смешивать.
$rawName = $input['name'] ?? null;
if (!is_string($rawName)) {
// Вернуть ошибку типа поля; trim массива здесь недопустим.
} else {
$normalizedName = trim($rawName);
$violations = $validator->validate($normalizedName, [
new Assert\NotBlank(), new Assert\Length(min: 2, max: 80),
]);
}
Документация: NotBlank и normalizer.
Документация: порядок ограничений и остановка цепочки.
DTO и PHP-атрибуты без полного Symfony
DTO — простой объект, в котором собраны входные данные. Рядом с полем удобно видеть его правила. Для такого формата в самостоятельном приложении нужно включить чтение атрибутов: иначе класс может выглядеть правильно, а ограничения не будут загружены.
final class ContactInput
{
#[Assert\Sequentially([
new Assert\Type('string'),
new Assert\NotBlank(normalizer: 'trim'),
new Assert\Email(normalizer: 'trim'),
])]
public mixed $email = null;
}
$dto = new ContactInput();
$dto->email = $input['email'] ?? null;
$objectValidator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator();
$violations = $objectValidator->validate($dto);
Входное свойство здесь имеет тип mixed намеренно. Если присвоить массив свойству string, PHP может выбросить TypeError ещё до запуска Validator. Сначала получите понятные ошибки входного контракта, затем переносите проверенные значения в строго типизированную модель приложения.
Указывайте начальные значения свойств. Не рассчитывайте, что неинициализированное поле автоматически выразит различие между «не прислано» и «прислан null». Если это различие важно для PATCH или обновления профиля, учитывайте наличие ключа отдельно от его значения.
Документация: загрузка атрибутов в standalone-компоненте.
Документация: Type и ограничения типизированных свойств PHP.
Группы и последовательность: одни данные, разные действия
Для черновика достаточно заголовка, а для публикации нужно описание. Такое отличие можно выразить группами, не копируя весь набор правил. Ограничения без явно заданной группы относятся к Default; при вызове другой группы она не подключается автоматически.
final class DraftInput
{
#[Assert\NotBlank]
public mixed $title = null;
#[Assert\NotBlank(groups: ['publish'])]
public mixed $description = null;
}
$draft = new DraftInput();
$draftErrors = $objectValidator->validate($draft);
$publishErrors = $objectValidator->validate($draft, null, ['Default', 'publish']);
Список групп запускает выбранные проверки, а GroupSequence добавляет порядок: следующая группа выполняется, только если предыдущая прошла. Например, нет смысла проверять готовность к публикации, пока отсутствует базовое название.
$sequence = new Assert\GroupSequence(['Default', 'publish']);
$violations = $objectValidator->validate($draft, null, $sequence);
Это передача последовательности непосредственно в validate(). При объявлении последовательности на классе используются дополнительные правила с группой имени класса. Не путайте группы проверки с ролями доступа: выбор publish не доказывает, что пользователь имеет право публиковать материал.
Документация: явный выбор групп проверки.
JSON API: 400, 422 и ошибки, пригодные для интерфейса
Запрос примеров содержит имя разрешённого сценария и данные. Некорректный JSON не дошёл до предметной проверки — это ошибка запроса. Корректный JSON с недопустимым значением поля получает 422. Успешная проверка возвращает 200; эти статусы выбирает обработчик, а не сам компонент Validator.
{
"scenario": "period",
"data": { "start": "2026-11-06", "end": "2026-11-05" }
}
Обработчик принимает только POST, ограничивает размер запроса и известные сценарии. В ответе есть valid, список violations, сводка и сведения о движке. Внутренний путь Symfony для массива выглядит как [items][0][quantity]; наш API приводит его к items.0.quantity. Это соглашение проекта, а не автоматическое требование библиотеки.
$errors = [];
foreach ($violations as $violation) {
$errors[] = [
'path' => $violation->getPropertyPath(),
'message' => $violation->getMessage(),
'code' => $violation->getCode(),
];
}
http_response_code($errors === [] ? 200 : 422);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['valid' => $errors === [], 'violations' => $errors],
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
Это минимальный фрагмент формирования ответа без преобразования пути. Не добавляйте к нему getInvalidValue() «для удобства отладки»: в поле может находиться пароль или другое значение, которое не нужно повторять в ответе. Сообщение должно описывать правило, а путь — помогать найти поле.
async function validateData(scenario, data) {
const response = await fetch('./api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ scenario, data })
});
if (response.status !== 200 && response.status !== 422) {
throw new Error('Проверка сейчас недоступна');
}
const result = await response.json();
if (typeof result.valid !== 'boolean' || !Array.isArray(result.violations)) {
throw new Error('Неожиданный ответ проверки');
}
return result;
}
В полноценной форме нужно также отменять устаревший запрос или игнорировать его ответ, если данные уже изменились. Сетевой сбой и нарушение правила — разные состояния. 422 разрешает пользователю исправить поля; отсутствие ответа не даёт оснований считать ввод ни корректным, ни некорректным.
После валидации остаются права доступа, CSRF для соответствующей модели сессии, запись в хранилище и безопасный вывод. Исключения удобно разбирать через Monolog, а настройки окружения — через phpdotenv. В журнал ошибок не стоит помещать весь запрос формы.
Как показать ошибки, чтобы форму действительно исправили
Одного красного заголовка «Ошибка» мало. Покажите краткую сводку, свяжите каждое сообщение с полем и сохраните введённые значения. Если ошибочных полей несколько, пользователь должен понимать объём работы, а не обнаруживать по одному новому отказу после каждого запроса.
function showFieldError(input, message, errorElement) {
errorElement.textContent = message;
errorElement.hidden = false;
input.setAttribute('aria-invalid', 'true');
const ids = new Set((input.getAttribute('aria-describedby') || '')
.split(/\s+/).filter(Boolean));
ids.add(errorElement.id);
input.setAttribute('aria-describedby', [...ids].join(' '));
}
Этот фрагмент добавляет сообщение, сохраняя связь с уже существующей подсказкой. При новой проверке устаревшую ошибку нужно снять. Пути из ответа сопоставляйте с известными полями, а текст помещайте через textContent. Не стройте HTML и CSS-селектор простым соединением непроверенной строки.
На телефоне подпись, значение и пояснение должны оставаться рядом; длинный JSON и код прокручиваются внутри своего блока. После отправки удобно перевести фокус к сводке или первому ошибочному полю, но не перехватывать его при каждом символе. Успешное завершение можно показать коротким уведомлением, например через SweetAlert2; сами ошибки формы лучше оставить непосредственно возле ввода.
Типичные ошибки и короткая проверка своего набора правил
| Проблема | Причина | Исправление |
|---|---|---|
| Пустой email проходит | Email проверяет формат необязательного значения | Добавить NotBlank там, где поле обязательно |
| Атрибуты не работают | Standalone builder не читает их автоматически | Включить enableAttributeMapping |
| Массив вызвал ошибку строковой функции | Нормализация раньше проверки типа | Сначала Type или явная проверка, затем trim/Length |
| Ноль считается отсутствием | Использовано empty() или нестрогое сравнение | Разделить null, пустую строку и допустимое 0 |
| Правила потерялись при выборе группы | Default не включён в вызов | Явно передать нужные группы |
| Ошибка попала не в ту строку | Перепутаны индекс массива и номер для человека | Сохранять путь и отдельно формировать подпись |
| Зелёный результат остался после правки | UI показывает ответ для старого ввода | Снять результат и выполнить новую проверку |
Для каждого правила возьмите корректное значение, ошибочное, пустоту и граничные случаи. Добавьте неожиданный тип: объект или массив вместо строки, строку вместо числа. Это важнее, чем проверять только подготовленный счастливый пример.
$cases = [
['value' => '', 'expected' => 1],
['value' => ' ', 'expected' => 1],
['value' => 'Анна', 'expected' => 0],
['value' => ['Анна'], 'expected' => 1],
];
foreach ($cases as $case) {
$actual = count($validator->validate($case['value'], $rules));
if ($actual !== $case['expected']) {
throw new RuntimeException('Неожиданный результат проверки');
}
}
В этом фрагменте $rules — строковая цепочка из раздела о нормализации. Для API отдельно проверьте синтаксически неверный JSON, неизвестный сценарий, превышение лимита и недоступность сервера. Ошибка в настройке проверяющего механизма не должна маскироваться под обычную ошибку поля.
Исходники, локальный запуск и официальные источники
Скачайте проект, распакуйте его и откройте терминал в каталоге с composer.json. Установите зависимости из lock-файла, затем запустите сервер с корнем public. Открытие HTML через file:// не запускает PHP и не заменяет этот шаг.
composer install
composer check-platform-reqs
php -S 127.0.0.1:8080 -t public
Скачать проект Symfony Validator с шестью примерами и мастерской. После запуска откройте http://127.0.0.1:8080/, повторите упражнение с известной ошибкой, затем замените подготовленные данные. Для внедрения начните с одной формы и общего формата ответа.
- Официальное руководство Symfony Validator 7.4 — правила, объекты и получение нарушений.
- Версия 7.4 LTS — требования PHP и сроки поддержки.
- Справочник ограничений — полный список правил и их параметры.
- Загрузка правил вне фреймворка — атрибуты и builder.
- Лицензия MIT — условия использования компонента.
Полезный результат этой работы — один понятный договор о данных. Форма помогает человеку ввести значение, PHP независимо проверяет его, ответ указывает точное место ошибки, а приложение выполняет действие только после успешной проверки и проверки прав. Этот договор одинаково пригодится для небольшого сайта, внутреннего сервиса и отдельного API.


