Skip to main content
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.

1. ControllerInterceptor

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.
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.
postHandle is not called if the handler method throws an exception. Use HandlerInterceptor::afterCompletion() if you need guaranteed post-request cleanup.

Example: Per-Controller Authentication Guard

OrderController.php

2. HandlerInterceptor

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.
1

Implement HandlerInterceptor

Create a class that implements HandlerInterceptor and define your logic in the three lifecycle methods.
RequestLoggingInterceptor.php
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

Path-Scoped Interceptor Example

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

InterceptorRegistry.addInterceptor

HandlerInterceptor
required
An instance of HandlerInterceptor to register.
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.
Winter Boot validates each regex at startup and throws InvalidSyntaxException for malformed patterns.

Execution Order

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.
Use afterCompletion for resource cleanup (closing connections, clearing thread-locals) because it is the only lifecycle hook guaranteed to run regardless of whether the request succeeded or failed.