Route interceptors
Route interceptors — глобальні pre-handler хуки, які ядро викликає в
RouteStrategy після middleware, але до
autowire та виклику контролера. Вони зручні для перевірки прав доступу (ACL),
політик і бізнес-обмежень на рівні імені маршруту.
Ядро надає лише інтерфейс RouteInterceptorInterface — типи винятків,
правила ACL і відповіді (403, redirect) визначає застосунок.
Див. також: Middleware, Маршрутизація, життєвий цикл → RouteStrategy.
Interceptors і middleware
| Route interceptors | Middleware | |
|---|---|---|
| Контракт | RouteInterceptorInterface::intercept() |
PSR-15 MiddlewareInterface::process() |
| Реєстрація | config/routes.php → routes.interceptors |
lazyMiddleware() у файлах маршрутів |
| Область | Глобально для всіх збігів маршруту | Глобально, група або один маршрут |
| Типова відмова | Кинути виняток (403, 401…) | Повернути ResponseInterface (redirect) |
| Приклад | ACL за setName() маршруту |
AuthMiddleware → redirect на login |
На практиці часто поєднують обидва підходи: middleware на групі /admin
перевіряє «чи залогінений» і робить redirect; interceptor перевіряє «чи дозволено цей
named route» і кидає виняток.
Реєстрація
Додайте класи interceptors у config/routes.php (компонент Acl надає готовий interceptor):
use Concept\Components\Acl\Authorization\AclRouteAuthorization;
return [
'routes' => [
'interceptors' => [
AclRouteAuthorization::class,
],
'list' => [
dirname(__DIR__) . '/routes/web.php',
],
],
];
Порядок у масиві — порядок виклику. Клас резолвиться з DI-контейнера
(залежності конструктора підставляються автоматично).
За telemetry.enabled кожен interceptor фіксується як
TelemetryEvent::HTTP_ROUTE_INTERCEPTOR_EXECUTED.
Приклад: компонент Acl
Interceptor отримує об’єкт League\Route\Route і PSR-7 запит.
Компонент Acl (Concept\Components\Acl\Authorization\AclRouteAuthorization)
перевіряє named route через правила з БД і кидає AccessDeniedException:
<?php declare(strict_types=1);
namespace Concept\Components\Acl\Authorization;
use Concept\Components\Acl\Authorization\Exceptions\AccessDeniedException;
use Concept\Components\Acl\Contracts\AclInterface;
use Concept\Components\Acl\Services\AclRouteRulesService;
use Concept\Core\Http\Contracts\RouteInterceptorInterface;
use Concept\Core\Services\Config\Contracts\ConfigInterface;
use League\Route\Route;
use Psr\Http\Message\ServerRequestInterface;
final class AclRouteAuthorization implements RouteInterceptorInterface
{
public function __construct(
private readonly AclInterface $acl,
private readonly AclRouteRulesService $routeRules,
private readonly ConfigInterface $config,
) {}
public function intercept(Route $route, ServerRequestInterface $request): void
{
$routeName = $route->getName();
if (!is_string($routeName) || $routeName === '') {
return;
}
$rule = $this->routeRules->resolve($routeName);
if ($rule === null) {
return;
}
$resource = $rule['resource'];
$privilege = $rule['privilege'] ?? null;
if ($this->acl->isAllowed($resource, $privilege)) {
return;
}
$redirectRouteName = $rule['redirect_route_name'] ?? null;
if (!is_string($redirectRouteName) || $redirectRouteName === '') {
$redirectRouteName = $this->config->getString('acl.redirect_route_name', 'admin.dashboard');
}
throw new AccessDeniedException(
sprintf('Access denied for route [%s]', $routeName),
$redirectRouteName,
);
}
}
Виняток компонента Acl (містить ім’я маршруту для redirect):
<?php declare(strict_types=1);
namespace Concept\Components\Acl\Authorization\Exceptions;
use RuntimeException;
final class AccessDeniedException extends RuntimeException
{
public function __construct(
string $message,
private readonly string $redirectRouteName,
) {
parent::__construct($message);
}
public function redirectRouteName(): string
{
return $this->redirectRouteName;
}
}
Що відбувається з винятком
Interceptor не повертає відповідь — лише кидає виняток або завершується без дій.
Необроблений виняток перехоплює Whoops (ErrorHandlerServiceProvider):
- JSON / AJAX —
RequestFormat::expectsJson()→JsonResponseHandler - Debug web —
app.debug→ Pretty Page - Production web — fallback з
PathName::ERRORS_FALLBACK_VIEWS
Для передбачуваної відповіді 403 або redirect краще обгорнути handler у middleware (наступний розділ), а не покладатися лише на Whoops.
Redirect і 403 через middleware
Компонент Acl надає HandleAccessDeniedMiddleware, який перехоплює
AccessDeniedException (за аналогією з
HandleValidationExceptionMiddleware) і повертає redirect або JSON:
<?php declare(strict_types=1);
namespace Concept\Components\Acl\Middlewares;
use Concept\Components\Acl\Authorization\Exceptions\AccessDeniedException;
use Concept\Core\Http\Contracts\ResponseFactoryInterface;
use Concept\Core\Http\Protocol\HttpStatusCode;
use Concept\Core\Http\Requests\RequestFormat;
use Concept\Core\Services\Session\Contracts\FlashBagInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class HandleAccessDeniedMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly ResponseFactoryInterface $responseFactory,
private readonly RequestFormat $requestFormat,
private readonly FlashBagInterface $flashBag,
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
try {
return $handler->handle($request);
} catch (AccessDeniedException $exception) {
if ($this->requestFormat->expectsJson($request)) {
return $this->responseFactory->jsonError(
$exception->getMessage(),
HttpStatusCode::FORBIDDEN
);
}
$this->flashBag->addError($exception->getMessage());
return $this->responseFactory->back(
HttpStatusCode::FOUND,
$exception->redirectRouteName()
);
}
}
}
Зареєструйте middleware зовні interceptor-ланцюжка (першим у файлі маршрутів):
use Concept\Components\Acl\Middlewares\HandleAccessDeniedMiddleware;
$router->lazyMiddleware(HandleAccessDeniedMiddleware::class);
$router->lazyMiddleware(HandleValidationExceptionMiddleware::class);
// ... інші middleware
Так interceptor лишається «тонким» (лише перевірка + throw), а формат відповіді (HTML redirect vs JSON 403) централізовано в middleware.
Альтернатива: лише middleware (redirect)
Якщо достатньо перевірки «залогінений / активний користувач» без ACL за named route,
redirect можна зробити без interceptor — наприклад
AuthMiddleware з компонента AuthAdmin:
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
if (!$this->auth->check()) {
return new RedirectResponse('/admin/login', HttpStatusCode::FOUND);
}
return $handler->handle($request);
}
Або через ResponseFactoryInterface::redirectByName() — див.
Middleware → приклади авторизації.
Поєднаний сценарій
flowchart TB
REQ["HTTP запит"]
MW1["HandleAccessDeniedMiddleware"]
MW2["AuthMiddleware на /admin"]
MATCH["Router match"]
INT["AclRouteAuthorization"]
CTRL["Controller"]
REQ --> MW1 --> MW2 --> MATCH --> INT
INT -->|"can access"| CTRL
INT -->|"AccessDeniedException"| MW1
MW2 -->|"not logged in"| REDIR["RedirectResponse login"]
MW1 -->|"catch"| RESP["JSON 403 або redirect"]
AuthMiddleware(AuthAdmin) на групі — груба перевірка сесії, redirect без винятку.AclRouteAuthorization(Acl) — тонка перевірка права на named route,throw.HandleAccessDeniedMiddleware(Acl) — перехоплює виняток, відповідає клієнту.
Поради
- Завжди іменуйте маршрути (
setName()), якщо ACL прив’язаний до імені. - Не дублюйте перевірку login у interceptor і middleware — розділіть ролі.
- Для API — JSON через
HandleAccessDeniedMiddleware+expectsJson(). - Interceptor не замінює CSRF —
VerifyCsrfTokenMiddlewareлишається в стеку.