Usage
Constructor
final class GuardMiddleware implements HttpKernelInterface, TerminableInterface
{
public function __construct(
HttpKernelInterface $kernel,
GuardEngine $engine
) { ... }
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response;
public function terminate(Request $request, Response $response): void;
}
GuardMiddleware decorates the kernel: construct it wherever your
App\Kernel is assembled (front controller or container). The constructor
calls $engine->initialize(); a GuardRedisException is swallowed only when
SecurityConfig.redisFailOpen is true, otherwise construction fails closed by
rethrowing. Block verdicts are translated into a Symfony Response with the
verdict status, body, and headers.
Attachment
// public/index.php
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
The middleware screens the MAIN kernel request only and passes sub-requests
straight into the wrapped kernel; sub-requests are derived from a main request
that was already screened, and screening them again would double-count
rate-limit hits. On a clean pass the original Request is forwarded to the
kernel untouched.
Verdicts
When the engine returns a block verdict, ResponseTranslator copies the
verdict status code, body, and headers onto a PSR-7 response, and never calls
the wrapped handler:
| Situation | Status | Body |
|---|---|---|
| Banned IP | 403 | IP address banned |
| Suspicious content | 400 | Suspicious activity detected |
| Rate limit exceeded | 429 | Too many requests plus Retry-After |
| Engine malfunction | 500 | Security check failed |
Bodies can be overridden globally through SecurityConfig.customErrorResponses.
Fail-closed behavior
If the engine check throws, the middleware catches it, and responds with the
engine's fail-closed response (500 Security check failed) rather than letting
the request through. A custom 500 body comes from
customErrorResponses[500], not from adapter code.
Route-scoped configuration
The engine supports per-route RouteConfig (required headers, per-route rate
limits, bypassed checks) keyed by a route ID on the request state. This adapter
builds the engine request internally, so there is no route-ID hook; the
engine-sanctioned equivalent is the customRequestCheck config closure, which
runs as the last pipeline check and can return a block verdict for any request
shape. See the advanced example app
for an admin gate built that way.
Testing your integration
The package ships its own suite (composer test); for your app, assert that a
blacklisted IP gets a 403 with the engine body and that clean requests reach
your handler with the original PSR-7 instance.