Http brick

Без HTTP capability немає маршрутів, контролерів і PSR-7 відповідей — лише «голий» Core без web-входу. addHttp() на ConceptStack вмикає саме це: kernel, router, response factory. Повертає HttpBuilder для того, що відрізняє ваш додаток: які route-файли, чи потрібні FormRequest у сигнатурі, чи JSON-404 замість League default. Мінімальна збірка — у рецепті Перший HTTP-запит.

Middleware lists живуть у routes/*.php, не в Stack: порядок pipeline — історія запиту, а не глобальний конфіг brick. Stack відповідає за «що взагалі вміє kernel», routes — за «як обробляється конкретна група URL».

Методи HttpBuilder:

Метод Параметри Опис Залежність
setRoutes() list<string> $routes Абсолютні шляхи до route-файлів ($router у scope) — (обов’язково)
setInterceptors() list<RouteInterceptorInterface|Closure(ContainerInterface): mixed> $interceptors Глобальні route interceptors перед invoke handler-а —
setNotFoundMiddleware() MiddlewareInterface|Closure(ContainerInterface): mixed|null $middleware Кастомний 404 middleware замість League default —
withFormRequests() — FormRequest у argument resolver chain addValidation()
withTypedRouteParameters() — Явний Valinor-cast route params (суворіше за Reflection; DTO з URL) addCasting()

addHttp()

$stack = ConceptStack::create();

$stack->addHttp()
    ->setRoutes([$rootPath . 'routes/web.php']);

Точка входу: реєструє HTTP capability і повертає builder. Без цього виклику View, ErrorHandling з HTML-fallback і UrlGenerator не мають сенсу — їм потрібен уже зібраний Http. Модель збірки — у огляді Stack.

setRoutes()

$stack->addHttp()
    ->setRoutes([
        $rootPath . 'routes/web.php',
        $rootPath . 'routes/api.php',
    ]);

Навіщо окремі файли, а не один гігантський список у Bootstrap: web і API мають різний middleware (CSRF / flash vs JSON), і їх зручно розвести. HttpKernel під час boot створює League $router, робить require кожного файла (змінна вже в scope) і реєструє маршрути. Порядок у масиві = порядок підключення. Без хоча б одного шляху збірка не пройде валідацію — інакше kernel «вміє HTTP», але не знає жодного URL. Приклад одного файла і GET / — у кроці 1.

setInterceptors()

$stack->addHttp()
    ->setRoutes([$rootPath . 'routes/web.php'])
    ->setInterceptors([
        static fn (ContainerInterface $c) => $c->get(AuthRouteInterceptor::class),
        static fn (ContainerInterface $c) => $c->get(LocaleRouteInterceptor::class),
    ]);

Коли логіка залежить від метаданих маршруту (name / path / метод), а не від «будь-якого» PSR-15 запиту — interceptors зручніші за middleware. Вони виконуються в RouteStrategy після match і перед resolve аргументів / викликом контролера (див. RouteInterceptorInterface). Типовий кейс — ACL за іменем маршруту. На відміну від middleware, список тут глобальний для всього kernel; per-route лишається в route-файлах. Порядок у масиві зберігається. Передавайте instance або Closure(ContainerInterface); class-string Core більше не резолвить — фабрика викликає $c->get(...) сама.

setNotFoundMiddleware()

$stack->addHttp()
    ->setRoutes([$rootPath . 'routes/api.php'])
    ->setNotFoundMiddleware(
        static fn (ContainerInterface $c) => $c->get(NotFoundJsonMiddleware::class),
    );

За замовчуванням League кидає NotFoundException — зручно для HTML з Whoops/fallback, погано для API-клієнта, який чекає JSON. Метод замінює 404 на власний PSR-15 middleware (instance або Closure(ContainerInterface): MiddlewareInterface — не class-string). Коли не викликати: лишається core default. Параметр потрапляє в RouteStrategy::getNotFoundDecorator().

withFormRequests()

$stack->addValidation();

$stack->addHttp()
    ->setRoutes([$rootPath . 'routes/web.php'])
    ->withFormRequests();

Opt-in саме тоді, коли action має приймати ContactFormRequest $form без ручного FormRequestFactory::make(). Додає FormRequestArgumentResolver на початок resolver chain. Stack робить require(http, validation) — потрібен addValidation(). Factory уже є після validation; цей метод потрібен лише для auto-injection. Без нього type-hint FormRequest просто не резолвиться — як у ранніх кроках рецепта, поки валідацію ще викликали вручну.

withTypedRouteParameters()

$stack->addCasting();

$stack->addHttp()
    ->setRoutes([$rootPath . 'routes/web.php'])
    ->withTypedRouteParameters();

Route vars з URL завжди string. Core викликає action через ReflectionMethod::invokeArgs: для простого int $id PHP у weak mode часто сам коерсить "1" → int — без цього opt-in і без Casting. Це side effect Reflection, не заявлений typed-routing API.

withTypedRouteParameters() вставляє TypedRouteParameterArgumentResolver (Valinor / Casting) перед raw RouteParameterArgumentResolver: явний cast до invokeArgs. Потребує addCasting(). Навіщо, якщо Reflection уже «робить int»:

  • суворіші правила (наприклад "1.5" → помилка, а не тихий 1)
  • CastingException замість голого TypeError
  • не лише scalar — DTO / value object з сегмента URL

Без opt-in лишаються базові resolvers з Core: сирий string у аргументах + можлива coercion Reflection на виклику.

Порядок argument resolvers

Порядок важливий: FormRequest має впасти до controller, якщо дані погані; typed route (якщо увімкнено) — до raw string resolver, щоб Valinor встиг до Reflection coercion. HttpStackProvider збирає ланцюжок так (деталі — Argument resolvers):

  1. FormRequestArgumentResolver — лише після withFormRequests()
  2. ServerRequestArgumentResolver — завжди (PSR-7 request)
  3. TypedRouteParameterArgumentResolver — лише після withTypedRouteParameters()
  4. RouteParameterArgumentResolver — завжди (сирі route vars)

Розширена збірка

$stack = ConceptStack::create();

$stack->addCasting();
$stack->addValidation()
    ->withLogging($rootPath . 'storage/logs/validation.log');

$stack->addHttp()
    ->setRoutes([
        $rootPath . 'routes/web.php',
        $rootPath . 'routes/api.php',
    ])
    ->setInterceptors([
        static fn (ContainerInterface $c) => $c->get(AuthRouteInterceptor::class),
    ])
    ->withTypedRouteParameters()
    ->withFormRequests()
    ->setNotFoundMiddleware(
        static fn (ContainerInterface $c) => $c->get(NotFoundJsonMiddleware::class),
    );

Capabilities реєструють окремими гілками від кореня $stack, не змішуючи fluent-контекст. Порядок add* у коді не визначає порядок providers: Stack топологічно ставить залежності (validation, casting) перед http — щоб не думати про це вручну.

Що дає extension-http

Builder не конфігурує сервіси пакета напряму: після addHttp() у контейнері з’являються Http ResponseFactory / UrlGenerator (і console route:list). Controller інжектить factory і пише body — без знання про Stack. Життєвий цикл від entry point до emit — у Життєвий цикл запиту.