ADR - Middleware¶
Overview¶
Middleware wraps an action, running code before and after it, or replacing it entirely. It is where cross-cutting concerns live: authentication, CORS, request identifiers, timing, and so on. The action stays focused on its one job while middleware handles everything around it.
Every middleware implements Phalcon\Contracts\ADR\Middleware:
public function __invoke(
Phalcon\Contracts\Http\AttributeRequestInterface $request,
Phalcon\Contracts\ADR\Handler $next
): Phalcon\Http\ResponseInterface;
A middleware receives the request and the $next handler in the chain. It can act before calling $next, act on the response afterwards, or skip $next altogether to short-circuit the request.
<?php
namespace MyApp\Middleware;
use Phalcon\Contracts\ADR\Handler;
use Phalcon\Contracts\ADR\Middleware;
use Phalcon\Contracts\Http\AttributeRequestInterface;
use Phalcon\Http\ResponseInterface;
final class ApiVersion implements Middleware
{
public function __invoke(AttributeRequestInterface $request, Handler $next): ResponseInterface
{
// run the rest of the pipeline, then act on the response
return $next($request)->setHeader('X-Api-Version', '1');
}
}
Stopping a request short (authentication, authorization) is covered in Halting execution below.
The pipeline¶
The dispatcher runs middleware through Phalcon\ADR\Pipeline, a self-recursive Handler. Each step hands the next middleware a fresh pipeline advanced by one as its $next, so $next is always a real handler, never a closure. When the middleware is exhausted, the pipeline invokes the terminal handler: the action itself.
Middleware runs in two scopes:
- Global middleware, configured on the dispatcher, wraps every request.
- Namespace middleware, configured on the router's middleware map, wraps only the actions under a namespace prefix.
Halting execution¶
A middleware stops a request before the action runs by not calling $next. It could build and return a response itself, but for a failure such as "forbidden" the cleaner path is to throw: the exception unwinds out of the pipeline to the application's error responder, which turns it into a response with the same machinery the rest of the app uses. The middleware never touches a status code or a response body, and the payload stays where it belongs, inside the domain and error paths.
The exception is a small class of your own, extending the ADR exception base:
<?php
namespace MyApp\Exception;
use Phalcon\ADR\Exceptions\Exception;
final class Forbidden extends Exception
{
}
Map it to a status on the error responder so it renders as a 403. This step is not optional: the error responder catches every thrown exception, but any it does not recognize renders as a 500, so an unmapped Forbidden would reach the client as a server error.
A minimal superadmin gate then reads:
final class RequireSuperadmin implements Middleware
{
public function __construct(private Identity $identity)
{
}
public function __invoke(AttributeRequestInterface $request, Handler $next): ResponseInterface
{
if (! $this->identity->isSuperadmin()) {
throw new Forbidden(); // the error responder renders the 403
}
return $next($request);
}
}
For real rules, lean on the ACL component. Build the policy once (in a provider) and inject it:
use Phalcon\Acl\Adapter\Memory;
$acl = new Memory(); // defaults to DENY
$acl->addRole('superadmin');
$acl->addRole('customer');
$acl->addComponent('invoices', ['list', 'view', 'void']);
$acl->allow('superadmin', 'invoices', '*');
$acl->allow('customer', 'invoices', ['list', 'view']); // note: not 'void'
The middleware reads the caller's role (established by your auth layer) and the resource and operation this route touches, and throws when the ACL says no:
final class Authorize implements Middleware
{
public function __construct(
private Memory $acl,
private Identity $identity
) {
}
public function __invoke(AttributeRequestInterface $request, Handler $next): ResponseInterface
{
$role = $this->identity->role(); // 'customer'
$component = (string) $request->getAttributes()->get('resource'); // 'invoices'
$action = (string) $request->getAttributes()->get('operation'); // 'void'
if (! $this->acl->isAllowed($role, $component, $action)) {
throw new Forbidden();
}
return $next($request);
}
}
A customer reaching POST /accounting/invoices/void/42 throws Forbidden, the error responder renders it as a 403, and the void action is never dispatched; a superadmin passes straight through. Attach each guard to the namespaces it should protect through the router's middleware map:
$router->setMiddlewareMap([
'\\Admin\\' => [RequireSuperadmin::class],
'\\Accounting\\' => [Authorize::class],
]);
Bundled middleware¶
Phalcon ships a handful of common middleware:
| Middleware | Effect |
|---|---|
Phalcon\ADR\Middleware\RequestIdMiddleware |
ensures every request carries an X-Request-Id, reusing an incoming one or generating a fresh value |
Phalcon\ADR\Middleware\TimingMiddleware |
adds an X-Response-Time header measuring the rest of the pipeline |
Phalcon\ADR\Middleware\MethodOverrideMiddleware |
enables the native _method override, only on POST and only for a safe verb (PUT/PATCH/DELETE) |
Phalcon\ADR\Middleware\CorsMiddleware |
config-driven CORS headers and preflight handling; ships inert until you configure an origin allowlist |