0% прочитано

Laravel: як замінити answers.{key} на реальні назви полів у динамічній валідації

У публічній формі ICanUp серверна валідація Laravel повертала технічне поле на кшталт answers.request_type замість зрозумілого користувачу «Тип запиту». Причина була не в перекладі validation.php, а в динамічній назві атрибута. Показую, як через FormRequest::attributes() підставити локалізовані назви полів для UK та EN без зміни структури answers.{key}.

17 вересня 2026 р. 9 хв читанняЛаравель

У публічній формі ICanUp серверна валідація Laravel працювала правильно, але повідомлення про помилку виглядало неправильно для користувача.

Замість назви поля він міг побачити технічний шлях на кшталт answers.request_type.

Для розробника це зрозумілий ключ. Для людини, яка просто заповнює форму, він нічого не означає.

Мені потрібно було залишити структуру запиту і правила Laravel без змін, але навчити валідатор використовувати реальну локалізовану назву динамічного поля.

Проблема була не в перекладі повідомлення required. Laravel уже вмів сказати «Поле :attribute є обов’язковим». Неправильним залишалося саме значення :attribute.
Laravel dynamic form validation maps answers.request_type to localized Request type and Тип запиту labels before rendering validation errors
Структура answers.{key} залишається технічною всередині запиту, але користувач у повідомленні про помилку бачить локалізовану назву поля.

Проблема стала видимою лише на серверній валідації

Форма могла містити поле з ключем:

Ключ динамічного поля
request_type

У запиті воно природно приходило як вкладене значення:

JSON - Структура даних публічної форми
{  "answers": {    "request_type": null  }}

Відповідне правило Laravel працювало з повним шляхом:

PHP - Правило серверної валідації
'answers.request_type' => [    'required',],

Тому без додаткової інформації Laravel використовував технічний шлях як назву атрибута.

Переклад validation.php не вирішує динамічну назву поля

Спочатку тут легко подумати, що достатньо додати або виправити переклади в lang/uk/validation.php.

Але шаблон повідомлення і назва поля - це дві різні частини.

Частина

Приклад

Шаблон повідомлення

Поле :attribute є обов’язковим.

Технічний атрибут

answers.request_type

Назва для користувача

Тип запиту

Laravel може правильно перекласти весь текст, але якщо :attribute залишається технічним, результат усе одно поганий.

Я не хотіла змінювати answers.{key}

Сам формат answers.{field_key} у цьому випадку був правильним.

Він дає стабільний зв’язок між визначенням поля форми та відповіддю користувача, незалежно від мови назви.

Наприклад, request_type може мати різні підписи:

Locale

Назва

Ключ

UK

Тип запиту

request_type

EN

Request type

request_type

Технічний ключ не повинен змінюватися разом із перекладом. Потрібно змінити лише те, як цей ключ показується у validation error.

FormRequest::attributes() виявився правильним місцем

Laravel FormRequest уже має механізм для власних назв атрибутів: метод attributes().

Саме його я використала у запиті публічної форми.

Ідея проста: для кожного активного поля форми побудувати відповідність між технічним шляхом і локалізованою назвою.

PHP - Найпростіший приклад attributes
public function attributes(): array{    return [        'answers.request_type' => 'Тип запиту',        'answers.email' => 'Електронна пошта',    ];}

Для статичної форми цього було б достатньо.

Але у мене reusable Forms, тому список полів заздалегідь невідомий.

Мапа атрибутів має будуватися з визначення форми

Правила і назви полів уже існують у визначенні форми. Тому немає сенсу дублювати їх у FormRequest.

Спрощено логіка виглядає так:

PHP - Побудувати назви атрибутів динамічно
public function attributes(): array{    $locale = app()->getLocale();     $attributes = [];     foreach ($this->formFields() as $field) {        $attributes["answers.{$field->key}"] =            $this->localizedFieldLabel(                $field,                $locale,            );    }     return $attributes;}
Це спрощений приклад структури. У реальному ICanUp SubmitPublicFormRequest використовує наявну систему перекладів FormField, а не окрему таблицю назв усередині request class.

Поточна locale повинна визначати назву поля

Одна й та сама форма може відкриватися українською або англійською.

Тому validation error повинен використовувати назву поля тієї самої мови, якою користувач зараз заповнює форму.

Один ключ, різні назви для користувача
UK requestanswers.request_type    ->Тип запиту EN requestanswers.request_type    ->Request type

Я не хочу отримати український текст помилки з англійською назвою поля або навпаки.

Тому назва береться через чинний механізм перекладів форми для активної locale запиту.

Запасна назва теж не повинна показувати answers.*

Є ще один важливий крайовий випадок: поле існує, але його назва для поточної мови відсутня або пошкоджена.

У такій ситуації я все одно не хочу показувати користувачу:

Небажаний запасний варіант
answers.request_type

Краще використати нейтральну зрозумілу назву.

Locale

Запасна назва

UK

Поле форми

EN

Field

PHP - Безпечна запасна назва
$label = $this->localizedFieldLabel(    $field,    $locale,); return filled($label)    ? $label    : __('forms.field');

attributes() не змінює самі правила валідації

Це одна з причин, чому мені подобається таке рішення.

Метод attributes() не змінює структуру даних і не послаблює правила. Він впливає лише на назву, яка підставляється у повідомлення.

PHP - Правило і назва атрибута залишаються окремими
$rules["answers.{$field->key}"] = [    'required',    'email',    'max:255',]; $attributes["answers.{$field->key}"] =    $localizedLabel;

Laravel і далі перевіряє answers.email. Користувач просто бачить Електронна пошта.

Одна назва автоматично працює для різних validation rules

Після того як Laravel знає правильну назву атрибута, її не потрібно окремо додавати для кожного правила.

Правило

Приклад результату

required

Поле «Електронна пошта» є обов’язковим.

email

Поле «Електронна пошта» має містити коректну електронну адресу.

min

Повідомлення використовує ту саму назву поля.

max

Повідомлення використовує ту саму назву поля.

Варіанти вибору

Помилка використовує назву select / option поля, а не answers.*

Checkbox

Помилка використовує назву checkbox-поля.

Особливо важливо не хардкодити назви конкретних полів

У reusable Forms адміністратор може створити нове поле без зміни PHP-коду.

Якби я додавала кожне ім’я вручну:

PHP - Підхід, який не масштабується
return [    'answers.request_type' => 'Тип запиту',    'answers.email' => 'Електронна пошта',    'answers.company' => 'Компанія',];

то кожне нове поле вимагало б зміни застосунку.

Це суперечить самій ідеї конструктора форм.

Назва має походити з того самого FormField, що й поле на сторінці

Я хочу, щоб підпис у формі та назва у validation error мали одне джерело.

Одне джерело назви поля
FormField translation        |        +-- public form label        |        +-- validation attribute name

Тоді перейменування поля в адмінці автоматично змінює і повідомлення про помилку.

Окремої синхронізації не потрібно.

Клієнтська перевірка не замінює серверну

Навіть якщо Vue вже показує required або перевіряє формат email до надсилання, сервер не може покладатися на це як на джерело істини.

Клієнтську перевірку можна обійти, а запит можна надіслати напряму.

Тому Form definition продовжує формувати серверні правила Laravel, а клієнтська частина лише покращує зручність.

Локалізація validation error не повинна перетворювати клієнтську перевірку на головну. Остаточне рішення про валідність запиту залишається на сервері.

attributes() не повинен дозволяти несподівані поля

Ще один важливий момент: наявність красивої назви атрибута не повинна змінювати контракт безпеки форми.

Якщо користувач надсилає поле, якого немає у визначенні форми, воно не стає дозволеним лише тому, що вкладене в answers.

JSON - Клієнт може надіслати більше, ніж показує форма
{  "answers": {    "request_type": "support",    "unexpected_admin_field": "1"  }}

Перелік допустимих ключів і правила валідації все одно повинні походити з актуального визначення Form.

Я перевіряла не один required

Такий bug легко виправити для одного прикладу і випадково залишити інші типи полів із технічними назвами.

Тому перевірка охоплює різні правила і типи динамічних полів.

Сценарій

Що перевіряється

UK required

Українська назва поля

EN required

Англійська назва поля

email

Той самий localized attribute

min / max

Назва не повертається до answers.*

select / options

Назва динамічного поля зберігається

checkbox

Повідомлення використовує назву FormField

Відсутній переклад

Людська запасна назва замість технічного ключа

Тест повинен перевіряти не тільки 422

Сам факт відповіді 422 Unprocessable Entity не доводить, що користувач отримав хороший текст.

Я хочу перевіряти і конкретне поле помилки, і те, що технічний ключ не потрапив у повідомлення.

PHP - Перевіряти не лише код відповіді
$response->assertUnprocessable(); $response->assertJsonValidationErrors([    'answers.request_type',]); $response->assertJsonMissing([    'The answers.request type field is required.',]);
Точна форма assertion залежить від структури відповіді застосунку. Суть тесту - технічний ключ може залишатися ключем validation bag, але не повинен з’являтися у видимому користувачу тексті.

Ключ помилки і текст помилки мають різні задачі

Я не намагалася замінити сам ключ answers.request_type у validation response.

Клієнтській частині цей шлях корисний: за ним вона розуміє, біля якого поля показати помилку.

Людським має бути саме повідомлення.

Частина помилки

Для кого

answers.request_type

Vue / application code

«Тип запиту»

Користувач

Що я не роблю після такого бага

  • Не перейменовую answers.{key} залежно від мови.

  • Не хардкодити назви кожного FormField у FormRequest.

  • Не створюю окрему систему validation messages лише для Forms.

  • Не замінюю серверну валідацію клієнтською.

  • Не показую raw answers.* як запасну назву.

  • Не використовую назву з іншої locale, якщо поточний переклад відсутній.

  • Не змінюю правила валідації лише заради красивішого повідомлення.

  • Не вважаю відповідь 422 достатньою перевіркою якості validation UX.

Мій короткий checklist для динамічної валідації

  • Залишити стабільний технічний ключ поля.
  • Будувати серверні rules із визначення Form.
  • Будувати attributes() із тих самих FormField.
  • Використовувати назву активної locale.
  • Мати зрозумілу запасну назву при відсутньому перекладі.
  • Не показувати користувачу raw answers.*.
  • Перевірити required, email, min, max і поля вибору.
  • Перевірити UK та EN окремо.
  • Залишити validation key стабільним для клієнтського коду.
  • Не змінювати контракт persistence або Communication integration через локалізацію повідомлення.

Технічний ключ потрібен застосунку. Назва поля потрібна користувачу. Laravel дозволяє зберегти обидва, не змішуючи їх.

Висновок

У випадку з формами ICanUp серверна валідація не була неправильною. Вона просто знала поле за технічним шляхом answers.{key} і не знала, як це поле називається для людини.

Переклад стандартних Laravel validation messages вирішує лише частину задачі. Для динамічних форм потрібно окремо визначити відображувані назви атрибутів.

FormRequest::attributes() дозволив залишити структуру запиту, правила валідації та validation bag незмінними, але підставляти назву реального FormField для поточної мови.

Якщо перекладу немає, користувач отримує нейтральну назву Поле форми, а не внутрішній шлях answers.request_type.

Для мене правильна локалізація валідації означає не тільки перекласти слово required. Вона означає, що користувач узагалі не повинен бачити внутрішню структуру даних застосунку.