10. Casting і DTO
До цього кроку додаток уже повний для демо: форми, API, БД,
помилки, логи. Але після валідації controller усе ще бачить
array рядків з HTTP —
$validated['email']. Бізнес-код хоче typed
об’єкт (ContactDto), а не «мішок» ключів.
Без спільного casting шару кожен action пише свій hydrate —
і роз’їжджаються крайні випадки.
Casting — опційна capability: Validation і
FormRequest працюють без неї (кроки 3–9 це довели).
Підключаємо, коли потрібен toDto() і typed
параметри маршруту (int $id з {id}).
Пакет — Valinor через
extension-casting-valinor;
у Stack — Casting brick.
Що додається і навіщо
-
addCasting()—CasterInterfaceу контейнері; FormRequest може викликатиtoDto() -
ContactDto+ContactFormRequest::$dtoClass— validated array → typed об’єкт -
withTypedRouteParameters()на Http —{id}у URL →int $idу action (GET /contact/{id}) -
contact-show.twig— перегляд збереженого запису -
Controllers:
insert($dto->toArray())замість ручного мапінгу полів
ApplicationStackBootstrap.php
addCasting() реєструємо явно. Cache dir — для
Valinor mapper cache; setDebug($debug) у dev
(FileWatchingCache), щоб зміни DTO підхоплювались без ручного
flush. Той самий $debug, що на кроці 7 для
ErrorHandling і Twig.
На View (як і з кроку 7) — setCacheDir +
setDebug($debug) для Twig compile cache.
$stack->addCasting()
->setCacheDir($rootPath . 'storage/cache/casting')
->setDebug($debug);
$stack->addHttp()
->setRoutes([$rootPath . 'routes/web.php', $rootPath . 'routes/api.php'])
->withFormRequests()
->withTypedRouteParameters();
withTypedRouteParameters() потребує
addCasting() — resolver кастить сегменти маршруту
до scalar type-hints у controller. Для
GET /contact/{id} action отримує
int $id, без ручного (int) $request->….
Повний API — Casting brick,
Http brick.
ContactDto + FormRequest
DTO наслідує Dto з пакета Casting і описує поля
після валідації. У FormRequest задаємо
$dtoClass — без цього toDto()
поверне null навіть із увімкненим caster.
final class ContactDto extends Dto
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $message,
) {}
}
final class ContactFormRequest extends FormRequest
{
protected ?string $dtoClass = ContactDto::class;
// rules() / messages() — як на кроці 3
}
Controllers
Web і API більше не читають $validated['…'] —
беруть $form->toDto(). Поля DTO збігаються з
колонками таблиці — insert($dto->toArray())
(created_at дає default у міграції).
public function submit(ContactFormRequest $form): ResponseInterface
{
$dto = $form->toDto();
$this->database->capsule()->table('contact_submissions')
->insert($dto->toArray());
}
public function showById(int $id): ResponseInterface
{
$row = $this->database->capsule()
->table('contact_submissions')->where('id', $id)->first();
// contact-show.twig: found / 404
}
Маршрут GET /contact/{id} у
routes/web.php — після seed/migrate відкрийте
/contact/1, щоб побачити typed $id в дії.
Запуск і перевірка
Тег step-10-casting. Після валідного POST контакт
зберігається через DTO (toDto() → toArray()).
Запуск
git checkout step-10-casting
rm -rf storage/cache storage/sessions storage/logs
composer install
cp docker-compose.yml.example docker-compose.yml
docker compose up -d --build
docker compose exec app php bin/console.php db:migrate
docker compose exec app php bin/console.php db:seed
Перевірка в браузері
- Відкрийте http://localhost:8089/contact, заповніть і надішліть форму — зелений alert з validated-даними.
- Перевірте http://localhost:8089/api/stats — лічильник submissions зріс.
-
Відкрийте
http://localhost:8089/contact/1
— сторінка submission (typed
{id}→int $id).
Перевірка з CLI
curl -sS -X POST http://127.0.0.1:8089/api/contact \
-H 'Content-Type: application/json' \
-d '{"name":"Ada","email":"ada@example.com","message":"Hello casting world"}'
# saved: true
curl -s http://localhost:8089/api/stats
# contact_submissions_total зріс
Ланцюжок
POST /contact (valid)
→ FormRequest validate
→ toDto() → ContactDto (Caster / Valinor)
→ insert($dto->toArray()) + view
GET /contact/{id}
→ withTypedRouteParameters: {id} → int $id
→ contact-show.twig (found або 404)
Далі — 11. Config:
literal paths і getenv у Bootstrap замінити на
іменовані шляхи й config keys.
Усі кроки — Огляд.
Brick — Casting.