Argument resolvers

Argument resolvers відповідають за те, як саме параметри методу handler-а отримують значення перед викликом. Без них RouteStrategy не знав би, звідки взяти ServerRequestInterface $request чи string $name з URL — і довелося б або магічний switch у ядрі, або вручну діставати все з request у кожному action.

Ядро проходить chain resolver-ів по черзі: перший, чий supports() повернув true, формує значення через resolve(). Навіщо саме chain: пріоритет і розширення (FormRequest, casting) без змін RouteStrategy — лише новий клас у масиві від збірки (Bootstrap / Stack).

Мета resolver-ів

  • Розділити відповідальність: кожен resolver обслуговує один тип аргументів.
  • Уникнути монолітного switch в RouteStrategy.
  • Керувати пріоритетом через порядок у масиві (FormRequest має йти до generic request).
  • Додавати нову логіку без змін ядра — власними resolver-класами.

Контракт

$request у resolve() — уже enriched (після prepareRequest(), з route attributes). $vars — сирі значення з $route->getVars(). Два джерела навмисно: іноді потрібен string з vars, іноді attribute / body з request.

interface ArgumentResolverInterface
{
    public function supports(ReflectionParameter $parameter, array $vars): bool;
    public function resolve(
        ReflectionParameter $parameter,
        ServerRequestInterface $request,
        array $vars,
    ): mixed;
}

Вбудовані resolvers

У Core лише два — мінімум для «request + URL params». Усе складніше (валідована форма, typed cast) живе в extensions і підставляється збіркою в той самий масив.

КласДля чогоsupportsresolve
ServerRequestArgumentResolver Інжектити весь поточний HTTP request у метод handler-а Тип параметра — ServerRequestInterface або підклас Enriched request
RouteParameterArgumentResolver Підставити route параметр за іменем ({id}, {slug}, ...) array_key_exists($parameter->getName(), $vars) Значення з $vars (string)

Який resolver коли спрацьовує

Порядок у сигнатурі методу не важливий для match — важливий порядок у chain і supports(). Нижче обидва параметри покриваються двома вбудованими resolvers:

// route: /hello/{name}
public function hello(ServerRequestInterface $request, string $name): ResponseInterface
{
    // $request → ServerRequestArgumentResolver
    // $name    → RouteParameterArgumentResolver
}

Реєстрація chain

Порожній chain = handler без аргументів з request/URL (лише constructor DI). Базовий набір — два core resolvers; розширення вставляють себе перед generic, щоб специфічний type-hint не «з’їв» ServerRequest resolver.

new HttpKernelServiceProvider(
    routePaths: [$routesFile],
    resolvers: [
        new ServerRequestArgumentResolver(),
        new RouteParameterArgumentResolver(),
    ],
);

Якщо два resolver-и теоретично можуть підтримати один параметр, спрацює той, що стоїть першим у масиві — тому порядок є контрактом.

Fallback

Якщо жоден resolver не підійшов: спочатку default value параметра (якщо є), інакше null. Якщо тип параметра не допускає null, під час invoke можливий TypeError. Навіщо м’який fallback: не ламати optional параметри; але для обов’язкових type-hint краще мати resolver у chain, ніж ловити TypeError у проді.

Власний resolver

Коли: аргумент з’являється часто (заголовок, tenant id, DTO) і не хочете діставати його вручну в кожному action. Додайте instance у масив resolvers у потрібній позиції — пріоритет без зміни коду ядра.

final class HeaderArgumentResolver implements ArgumentResolverInterface
{
    public function supports(ReflectionParameter $parameter, array $vars): bool
    {
        return $parameter->getName() === 'userAgent';
    }

    public function resolve(
        ReflectionParameter $parameter,
        ServerRequestInterface $request,
        array $vars,
    ): mixed {
        return $request->getHeaderLine('User-Agent');
    }
}

Приклад handler

// GET /hello/{name}
public function hello(ServerRequestInterface $request, string $name): ResponseInterface