Route interceptors

Route interceptor — pre-handler хук у RouteStrategy. Він викликається для кожного matched route після middleware-ланцюжка League Route, але до prepareRequest() і до резолву аргументів handler-а.

Ключова ідея: одне централізоване місце для глобальних правил доступу, коли логіка спирається саме на метадані маршруту (name, vars, HTTP-метод, path), а не на response pipeline. Не треба дублювати «чи потрібен токен» у кожному route file — і не треба писати middleware, яке все одно лише кидає exception.

Для чого використовувати interceptors

  • Global pre-check для всіх маршрутів: токен, роль, feature flag, tenant-контекст.
  • Перевірка за іменем маршруту через $route->getName() без дублювання правил у кожному route file.
  • Fail-fast: зупинити обробку до створення аргументів handler-а (дешевше і чистіше, ніж «напівстворений» FormRequest).
  • Наскрізні аудиторські дії: контроль доступу, аудит спроб, стандартизовані domain-винятки.

Не використовуйте interceptor, якщо потрібно повернути або модифікувати ResponseInterface у ланцюжку — тоді PSR-15 middleware. Interceptor повертає void: або пропускає, або кидає.

Контракт

Ядро не нав’язує policy-модель: interceptor сам вирішує, кидати виняток чи пропускати запит далі. У intercept() доступні $route->getName(), $route->getVars() і container request (ще без route attributes — attributes з’являться після prepareRequest()).

namespace Concept\Core\Http\Contracts;

interface RouteInterceptorInterface
{
    public function intercept(Route $route, ServerRequestInterface $request): void;
}

Реєстрація

Interceptors реєструють у constructor HttpKernelServiceProvider (або через setInterceptors на Http brick у Stack) — глобально на всі matched routes. Передавайте готовий instance або Closure(ContainerInterface); class-string Core не резолвить. Типовий патерн — фабрика з $c->get(...), щоб DI зібрав залежності interceptor-а.

new HttpKernelServiceProvider(
    routePaths: [$routesFile],
    resolvers: [...],
    interceptors: [
        static fn (ContainerInterface $c) => $c->get(RequireAuthInterceptor::class),
    ],
);

Приклад

Типовий сценарій: немає Authorization → domain exception → ваше middleware / ErrorHandling перетворює на 401/403. Controller авторизацію «на всяк випадок» уже не дублює.

final class RequireAuthInterceptor implements RouteInterceptorInterface
{
    public function intercept(Route $route, ServerRequestInterface $request): void
    {
        $token = $request->getHeaderLine('Authorization');
        if ($token === '') {
            throw new UnauthorizedException('Missing token.');
        }
    }
}

Порядок виконання

Interceptor працює в момент, коли route вже знайдено, але handler ще не запущено — ідеальне місце для «чи взагалі можна йти далі».

HTTP request
  → League Route middleware chain
    → RouteStrategy::invokeRouteCallable()
      → runInterceptors()
      → prepareRequest()
      → resolveArguments()
      → invoke handler

Interceptors vs middleware

InterceptorMiddleware
КонтрактRouteInterceptorInterfacePSR-15
Де реєструватиHttpKernelServiceProvider constructorRoute file
ОбластьГлобально на всі matched routesPer route / group
ResponseЧерез виняток або side-effectПовертає ResponseInterface
Найкращий use-caseACL/policy на рівні routeresponse pipeline, трансформація відповіді

Події

Після кожного interceptor, якщо зареєстрований EventDispatcherInterface, диспатчиться RouteInterceptorInvoked з class-string interceptor і об’єктом route. Коли: аудит / метрики без змін самого interceptor. Коли ні: немає dispatcher — ядро просто пропускає подію.