Контролери

У документації «контролер» — клас-handler з публічними методами, на які вказують маршрути. Окремого базового Controller у Core немає: навіщо зайвий шар, якщо League Route і так резолвить instance з DI, а параметри методу — через argument resolvers.

Тримайте handler тонким: бізнес-кроки сторінки в методі, інфраструктура (CSRF, flash, shared view data) — у middleware / extensions. Так легше читати історію endpoint і тестувати без «глобальних хелперів».

Прив’язка до маршруту

Class-string у route file — контракт: контейнер створить handler, ви не робите new у routes. Це дозволяє підміняти залежності в тестах і не тягнути wiring у HTTP surface.

$router->get('/ping', [PingHandler::class, 'ping']);
$router->get('/hello/{name}', [PingHandler::class, 'hello']);

Constructor injection

Залежності конструктора резолвляться ReflectionContainer delegate — якщо сервіси зареєстровані або самі автовайряться. Контракти (інтерфейси) зареєструйте у власному service provider: Reflection не знає, яку реалізацію обрати.

final class PingHandler
{
    public function __construct(
        private readonly ClockInterface $clock,
    ) {}

    public function ping(): ResponseInterface
    {
        return new JsonResponse(['time' => $this->clock->now()]);
    }
}

Параметри методу

Те, що змінюється від запиту до запиту (request, {name}), — у сигнатурі методу, не в constructor. Інакше shared controller тримав би «минулий» request. ServerRequestInterface — через ServerRequestArgumentResolver; string $name — через RouteParameterArgumentResolver (плейсхолдер {name}).

public function hello(ServerRequestInterface $request, string $name): ResponseInterface
{
    return new JsonResponse([
        'name' => $name,
        'path' => $request->getUri()->getPath(),
    ]);
}

Відповідь

Handler повертає ResponseInterface. Інакше RouteStrategy кидає RuntimeException — ядро не вгадує, чи ви хотіли JSON чи HTML. На рівні Core зручні три базові формати Diactoros; ResponseFactory з Http extension — коли збірка вже підключила brick Http.

use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\Response\TextResponse;

return new JsonResponse(['ok' => true]);
return new HtmlResponse('<h1>Hello</h1>', 200);
return new TextResponse('pong', 200);

Приклад контролера для HTML

Рядок HTML у handler — ок для мінімального демо й health page. Коли з’являться layout, partials, escaping у багатьох місцях — підключайте View extension (Twig тощо); це вже поза межами ядра, і так і має бути.

use Laminas\Diactoros\Response\HtmlResponse;
use Psr\Http\Message\ResponseInterface;

final class PageHandler
{
    public function home(): ResponseInterface
    {
        $html = '<!doctype html><html lang=\"uk\"><body>'
            . '<h1>Мінімальна HTML-сторінка</h1>'
            . '<p>Сторінка віддана напряму з core handler.</p>'
            . '</body></html>';

        return new HtmlResponse($html, 200);
    }
}

RequestContext

Під час handler RequestContextInterface містить enriched request. Коли корисно: сервіс глибоко в call stack потребує поточного request, а прокидати параметр через усі шари боляче. Не зловживайте — для action достатньо type-hint у методі. Після handler context скидається (reset()), щоб наступний запит не бачив чужих attributes.

$enriched = ContainerDependency::get($container, RequestContextInterface::class)->current();