Skip to content

HTTP Interceptors: Pre- and Post-Processing in Winter Boot

Before a request reaches a REST controller method — and again after the method returns — Winter Boot passes the request through a chain of interceptors. Interceptors are the right place for cross-cutting concerns such as authentication, authorisation, rate limiting, audit logging, and response decoration. They operate only on requests destined for #[RestController] classes; static assets and non-controller routes bypass the interceptor pipeline entirely.

Winter Boot provides two distinct interception models. Choose the one that best matches the scope you need.

ControllerInterceptor

Scoped to a single controller class. Implement it directly on your controller. No registration required.

HandlerInterceptor

Scoped to the whole application. Registered in a WebMvcConfigurer bean and matched to requests by URI regex.

ControllerInterceptor (namespace dev\winterframework\core\web\ControllerInterceptor) is applied by having your controller itself implement the interface. Every request routed to that controller passes through its preHandle and postHandle methods, regardless of which handler method is invoked.

interface ControllerInterceptor
{
public function preHandle(
HttpRequest $request,
ResponseEntity $response,
ReflectionMethod $handler
): bool;
public function postHandle(
HttpRequest $request,
ResponseEntity $response,
ReflectionMethod $handler
): void;
}

The $handler argument is the ReflectionMethod instance for the specific handler method that is about to be (or has just been) called — useful when you need to inspect attributes or method-level metadata.

Method Called when Return value
preHandle() Before the handler method executes true → continue; false → abort
postHandle() After the handler method executes successfully N/A — return type is void

Example: Per-Controller Authentication Guard

Section titled “Example: Per-Controller Authentication Guard”
OrderController.php
<?php
namespace dev\winterboot\samples\controller;
use dev\winterframework\core\web\ControllerInterceptor;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\web\GetMapping;
use dev\winterframework\web\http\HttpRequest;
use dev\winterframework\web\http\ResponseEntity;
use ReflectionMethod;
#[RestController]
class OrderController implements ControllerInterceptor
{
#[Autowired]
protected AuthService $authService;
// --- ControllerInterceptor ---
public function preHandle(
HttpRequest $request,
ResponseEntity $response,
ReflectionMethod $handler
): bool {
$token = $request->getFirstHeader('Authorization');
if (!$this->authService->isValid($token)) {
$response->withStatus(\dev\winterframework\web\http\HttpStatus::$UNAUTHORIZED)
->withJson(['error' => 'Unauthorized']);
return false; // stop processing — no controller method is called
}
return true; // continue to the handler method
}
public function postHandle(
HttpRequest $request,
ResponseEntity $response,
ReflectionMethod $handler
): void {
// Runs after the handler method; useful for response decoration
}
// --- Handler methods ---
#[GetMapping(path: '/orders')]
public function listOrders(): array
{
return [];
}
}

HandlerInterceptor (namespace dev\winterframework\core\web\HandlerInterceptor) provides application-wide interception. You register interceptors in a WebMvcConfigurer bean and associate each one with one or more URI regex patterns. Only requests whose URI matches a registered pattern pass through that interceptor.

interface HandlerInterceptor
{
public function preHandle(HttpRequest $request, ResponseEntity $response): bool;
public function postHandle(HttpRequest $request, ResponseEntity $response): void;
public function afterCompletion(
HttpRequest $request,
ResponseEntity $response,
?Throwable $ex = null
): void;
}
Method Called when Return value
preHandle() Before the handler method (and before ControllerInterceptor::preHandle) true → continue; false → abort, response sent by this interceptor
postHandle() After the handler method executes successfully void
afterCompletion() After the response is rendered, even if an exception occurred void; $ex is non-null when an exception was thrown
  1. Implement HandlerInterceptor

    Create a class that implements HandlerInterceptor and define your logic in the three lifecycle methods.

    RequestLoggingInterceptor.php
    <?php
    namespace dev\winterboot\samples\interceptor;
    use dev\winterframework\core\web\HandlerInterceptor;
    use dev\winterframework\util\log\Wlf4p;
    use dev\winterframework\web\http\HttpRequest;
    use dev\winterframework\web\http\ResponseEntity;
    use Throwable;
    class RequestLoggingInterceptor implements HandlerInterceptor
    {
    use Wlf4p; // provides self::logInfo(), self::logError(), etc.
    public function preHandle(HttpRequest $request, ResponseEntity $response): bool
    {
    self::logInfo('Incoming ' . $request->getMethod() . ' ' . $request->getUri());
    return true; // always continue
    }
    public function postHandle(HttpRequest $request, ResponseEntity $response): void
    {
    self::logInfo('Handled ' . $request->getUri()
    . ' → HTTP ' . $response->getStatus()->getValue());
    }
    public function afterCompletion(
    HttpRequest $request,
    ResponseEntity $response,
    ?Throwable $ex = null
    ): void {
    if ($ex !== null) {
    self::logError('Exception during ' . $request->getUri() . ': ' . $ex->getMessage());
    }
    self::logInfo('Request complete: ' . $request->getUri());
    }
    }
  2. Register with WebMvcConfigurer

    Annotate a class with #[Configuration] and implement WebMvcConfigurer (namespace dev\winterframework\core\web\config\WebMvcConfigurer). Call $registry->addInterceptor() inside addInterceptors() to register each interceptor with its URI patterns.

    MyWebConfigurer.php
    <?php
    namespace dev\winterboot\samples\config;
    use dev\winterframework\core\web\config\InterceptorRegistry;
    use dev\winterframework\core\web\config\WebMvcConfigurer;
    use dev\winterframework\stereotype\Configuration;
    use dev\winterboot\samples\interceptor\RequestLoggingInterceptor;
    use dev\winterboot\samples\interceptor\AdminAccessInterceptor;
    #[Configuration(name: 'webMvcConfigurer')]
    class MyWebConfigurer implements WebMvcConfigurer
    {
    public function addInterceptors(InterceptorRegistry $registry): void
    {
    // Apply to every URI path
    $registry->addInterceptor(new RequestLoggingInterceptor(), '.*');
    // Apply only to /admin/* and /super/* paths
    $registry->addInterceptor(
    new AdminAccessInterceptor(),
    '^\/admin\/.*',
    '^\/super\/.*'
    );
    }
    }

The AdminAccessInterceptor below demonstrates how to enforce role-based access for a subset of URIs.

AdminAccessInterceptor.php
<?php
namespace dev\winterboot\samples\interceptor;
use dev\winterframework\core\web\HandlerInterceptor;
use dev\winterframework\web\http\HttpRequest;
use dev\winterframework\web\http\HttpStatus;
use dev\winterframework\web\http\ResponseEntity;
use Throwable;
class AdminAccessInterceptor implements HandlerInterceptor
{
public function preHandle(HttpRequest $request, ResponseEntity $response): bool
{
$role = $request->getFirstHeader('X-User-Role');
if ($role !== 'admin') {
$response->withStatus(HttpStatus::$FORBIDDEN)
->withJson(['error' => 'Admin access required']);
return false; // short-circuit — no further interceptors or controller called
}
return true;
}
public function postHandle(HttpRequest $request, ResponseEntity $response): void {}
public function afterCompletion(
HttpRequest $request,
ResponseEntity $response,
?Throwable $ex = null
): void {}
}
public function addInterceptor(HandlerInterceptor $interceptor, string ...$regexPaths): void;
  • $interceptor HandlerInterceptor (required) An instance of HandlerInterceptor to register.

  • $regexPaths string (required) One or more PHP regex patterns (without delimiters). Only requests matching at least one pattern are passed to this interceptor. Use '.*' to match every request, or anchored patterns like '^\/api\/.*' to restrict an interceptor to a sub-tree of your API.

When multiple interceptors are registered, they run in registration order for preHandle and in reverse registration order for postHandle and afterCompletion.

  1. HandlerInterceptor::preHandle

    First registered runs first. If any preHandle returns false, the chain stops immediately — no further preHandle calls are made, and the controller method is not invoked.

  2. ControllerInterceptor::preHandle

    Runs after all HandlerInterceptor::preHandle calls have returned true. This is the per-controller gate check.

  3. Controller method executes

    The matched handler method runs and produces a return value.

  4. HandlerInterceptor::postHandle

    Last registered runs first (reverse order). Only called when no exception was thrown.

  5. HandlerInterceptor::afterCompletion

    Last registered runs first (reverse order). Always called — even when an exception occurred. Only interceptors that already completed their preHandle step have their afterCompletion invoked.