3. Валідація
HTML-форма без перевірки полів швидко приймає сміття: порожнє ім’я,
«email» без @, коротке повідомлення. Можна перевіряти в controller
купою if — працює на одній сторінці, але правила
розмазуються по action-ах і погано діляться з API. Тому валідацію
виносимо в окремий клас правил (ContactFormRequest):
controller вирішує лише, що показати при помилці чи успіху.
Capability addValidation() підключаємо явно — без неї
factory FormRequest у контейнері немає. CSRF ще немає (крок 5):
спочатку видно чисту валідацію вводу. ErrorHandling теж пізніше —
тому помилки форми показуємо вручну в контролері, без прихованого
exception→flash middleware. Так легше зрозуміти обидві гілки відповіді,
перш ніж «сховати» їх за type-hint.
Що додається і навіщо
-
addValidation()— validator (Rakit) + FormRequest factory; правила живуть у класах, не в анонімних масивах у action -
ContactFormRequest— rules і повідомлення в одному місці: UI і API можуть ділити той самий клас -
GET|POST /contact— окрема сторінка демо, щоб не змішувати промо-головну з формою -
index.twig— посилання на/contactу nav, hero CTA і блоці Flow (крок 3) -
public/.htaccess— rewrite наindex.php(інакше Apache не дійде до Router для/contact)
withFormRequests() на Http не потрібен
для цього кроку: він лише додає argument resolver для type-hint у
action. Ми викликаємо factory вручну, щоб побачити гілку
«невалідно → знову відмалювати форму» без залежності від
ErrorHandling. Type-hint з’явиться на кроці 7, коли буде
middleware на ValidationException.
ApplicationStackBootstrap.php
Ми викликаємо addValidation(), щоб ізолювати перевірку
вводу: той самий validator і FormRequest factory потім знадобляться
і web-формі, і JSON API. Http і View лишаються як були — валідація
не тягне за собою session чи CSRF «за компанію». Порядок у Bootstrap
тут не критичний для HTTP, але Validation зручно ставити рано:
вона не залежить від View.
$stack->addValidation();
$stack->addHttp()
->setRoutes([$rootPath . 'routes/web.php']);
$stack->addView()
->withTwig()
->setViewsPath($rootPath . 'resources/views');
addValidation() уже реєструє FormRequest factory.
Опційний лог — withLogging($path) на Validation builder
(не плутати з capability addLogging()).
Повний API — у Validation brick.
ContactFormRequest.php
Правила й тексти помилок тримаємо поруч із назвою форми: контролер
не дублює рядки required|email, а UI може посилатися
на ті самі ключі помилок (errors.email тощо). Коли
з’явиться API (крок 4), той самий клас піде в JSON-handler —
без копіпасти правил.
final class ContactFormRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|min:2',
'email' => 'required|email',
'message' => 'required|min:10',
];
}
public function messages(): array
{
return [
'name:required' => 'Please enter your name.',
// …
];
}
}
Пакети — Form Request і Validation.
ContactController.php
Явний validate() у action показує обидві гілки відповіді
(422 з errors/old або 200 з validated) без exception magic. Це
навмисно «гучніше», ніж type-hint: ви бачите, що відбувається при
помилці, до того як крок 7 сховає це в middleware.
old передаємо з $form->all(), щоб користувач
не набирав поля з нуля після 422. Flash/session ще немає — усе
в тому ж POST-response.
public function submit(ServerRequestInterface $request): ResponseInterface
{
$form = $this->formRequests->make(ContactFormRequest::class, $request);
if (!$form->validate()) {
return $this->view->create('contact', [
'errors' => $form->errors(),
'old' => $form->all(),
'success' => false,
'validated' => null,
], HttpStatusCode::UNPROCESSABLE_ENTITY);
}
return $this->view->create('contact', [
'errors' => [],
'old' => [],
'success' => true,
'validated' => $form->validated(),
]);
}
Альтернатива з withFormRequests() — type-hint
ContactFormRequest у action: resolver кине
ValidationException, а ErrorHandling (крок 7)
віддасть відповідь. Зараз ми цього навмисно не робимо.
routes/web.php
Два маршрути на одну форму: GET показує порожню сторінку, POST
приймає дані. Імена (contact, contact.submit)
знадобляться пізніше для URL generator і redirect back.
$router->get('/contact', [ContactController::class, 'show'])->setName('contact');
$router->post('/contact', [ContactController::class, 'submit'])->setName('contact.submit');
resources/views/index.twig
Промо-головна лишається окремо від форми. У верхній панелі —
кнопка Contact на /contact; у hero
поруч із «How it works» — GitHub на репозиторій
рецептів (не дублюємо форму в hero).
<a class="btn-promo btn-promo-ghost" href="/contact">Contact</a>
…
<a class="btn-promo btn-promo-ghost" href="https://github.com/php-concept/concept-recipes" target="_blank" rel="noopener noreferrer">
<i class="bi bi-github me-1" aria-hidden="true"></i> GitHub
</a>
resources/views/contact.twig
Форма читає errors.* і old.* з даних,
які контролер передав явно — немає session flash «з повітря».
Після успіху показуємо validated-дані з того ж response. Коли
з’являться CSRF і redirect (кроки 5 і 7), ті самі ключі в шаблоні
лишаться; зміниться лише, звідки їх бере controller / middleware.
Запуск і перевірка
Тег step-03-validation містить увесь код до цього кроку
включно. Нижче — повний запуск з нуля. Очікуємо: невалідний POST →
422 і помилки в UI; валідний POST → 200
і зелений alert з validated-даними.
Запуск
git checkout step-03-validation
rm -rf storage/cache storage/sessions storage/logs
composer install
cp docker-compose.yml.example docker-compose.yml
docker compose up -d --build
Перевірка в браузері
- Відкрийте http://localhost:8089/ і натисніть Contact у nav (або перейдіть на /contact).
-
Надішліть порожню або невалідну форму (наприклад email
bad, коротке повідомлення) — поля зis-invalid, тексти помилок під полями. - Заповніть коректно: ім’я (≥2 символи), email, повідомлення (≥10 символів) — зелений alert OK. з validated-значеннями.
Перевірка з CLI
Те саме, що в браузері, але через HTTP-коди — зручно для CI або швидкої перевірки без UI.
curl -sI http://localhost:8089/contact | head -5
# Content-Type: text/html
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8089/contact \
-d 'name=a&email=bad&message=hi'
# 422
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8089/contact \
-d 'name=Anna&email=anna@example.com&message=Hello there friend'
# 200
Ланцюжок
Validation живе всередині action, не в middleware. Тому ланцюжок короткий: Router → controller → factory → validate → Twig.
addValidation() + addHttp() + addView()
POST /contact
→ ContactController::submit()
→ FormRequestFactory::make(ContactFormRequest)
→ validate() → errors + old або validated()
→ contact.twig (422 або 200)
Крок 4 — JSON API (окремий api-трек): той самий FormRequest, інший транспорт. Далі на web — Session і CSRF. Усі кроки — Огляд.