Category:
Tutorials & Guides || Posted Oct 22, 2026
At the core of virtually every modern web framework—whether it is Express, Laravel, or Symfony—lies two foundational software design patterns: the Front Controller and the Middleware Pipeline (often called the Onion Architecture).
The Front Controller acts as the single entry point for all incoming HTTP requests to an application. Instead of directing users to different physical files, web servers route every request to a single file (typically index.php or server.js). The Front Controller then passes the request through a pipeline of middleware layers—handling tasks like authentication, logging, CORS, and rate limiting—before delivering it to the final route handler.
Building your own Front Controller and middleware stack is the best way to understand how modern frameworks handle HTTP requests behind the scenes. Here is how to construct a lightweight, framework-agnostic middleware pipeline using Object-Oriented Programming and modern standards.
The Middleware Architecture Explained
Think of a middleware stack like a series of concentric layers or doors around your core application logic:
- An HTTP Request enters the outer layer.
- Each middleware inspects or modifies the request.
- The middleware decides whether to pass the request deeper into the pipeline or short-circuit execution and return an immediate Response (for instance, returning a 401 Unauthorized error).
- Once the innermost handler generates a Response, it travels back out through the same middleware layers in reverse order, allowing each layer to modify the outgoing response (such as adding HTTP headers or calculating execution time) before sending it back to the client.
To maintain interoperability and clean code, modern PHP implementations adopt the PSR-15 (HTTP Server Handlers) specification, which defines standard interfaces for request handlers and middleware.
Step 1: Define the Core Interfaces
To build an extensible stack, define two contract interfaces: one for the middleware layers and one for the pipeline request handler that manages execution.
The Request Handler Interface
The Request Handler processes an incoming request and returns a response.
PHP
namespace App\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface;
}
The Middleware Interface
Each middleware receives the current request and a reference to the next handler in the chain.
PHP
namespace App\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface;
}
Step 2: Implement the Middleware Pipeline Dispatcher
The pipeline dispatcher executes registered middleware sequentially. Using an immutable approach, each step in the pipeline wraps the remaining middleware into a new request handler.
PHP
namespace App\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class Pipeline implements RequestHandlerInterface
{
/** @var MiddlewareInterface[] */
private array $middlewareStack = [];
private RequestHandlerInterface $fallbackHandler;
public function __construct(RequestHandlerInterface $fallbackHandler)
{
$this->fallbackHandler = $fallbackHandler;
}
/**
* Add a middleware layer to the outer stack.
*/
public function pipe(MiddlewareInterface $middleware): self
{
$this->middlewareStack[] = $middleware;
return $this;
}
/**
* Executes the next middleware in the queue or triggers the fallback handler.
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (empty($this->middlewareStack)) {
return $this->fallbackHandler->handle($request);
}
// Shift the top middleware off the stack
$middleware = array_shift($this->middlewareStack);
// Create an anonymous inner handler that represents the remainder of the pipeline
$nextHandler = new class($this->middlewareStack, $this->fallbackHandler) implements RequestHandlerInterface {
private array $stack;
private RequestHandlerInterface $fallback;
public function __construct(array $stack, RequestHandlerInterface $fallback)
{
$this->stack = $stack;
$this->fallback = $fallback;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$pipeline = new Pipeline($this->fallback);
foreach ($this->stack as $middleware) {
$pipeline->pipe($middleware);
}
return $pipeline->handle($request);
}
};
return $middleware->process($request, $nextHandler);
}
}
Step 3: Create Practical Middleware Layers
With the pipeline in place, you can write decoupled, single-responsibility middleware components.
1. Execution Timing Middleware
This middleware measures processing duration and appends a custom header to the outgoing response.
PHP
namespace App\Http\Middleware;
use App\Http\MiddlewareInterface;
use App\Http\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class ResponseTimeMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$startTime = microtime(true);
// Pass the request to the next layer in the stack
$response = $handler->handle($request);
$executionTime = sprintf('%.2fms', (microtime(true) - $startTime) * 1000);
// Modify the outgoing response on the way back out
return $response->withHeader('X-Response-Time', $executionTime);
}
}
2. Bearer Token Authentication Middleware
This layer inspects the incoming request headers and short-circuits execution if the token is missing or invalid.
PHP
namespace App\Http\Middleware;
use App\Http\MiddlewareInterface;
use App\Http\RequestHandlerInterface;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class AuthenticationMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$authHeader = $request->getHeaderLine('Authorization');
if (!str_starts_with($authHeader, 'Bearer ') || substr($authHeader, 7) !== 'secret-api-token') {
// Short-circuit the pipeline and return an immediate response
return new JsonResponse([
'status' => 'error',
'message' => 'Unauthorized access'
], 401);
}
return $handler->handle($request);
}
}
Step 4: Assemble the Front Controller
The Front Controller (public/index.php) captures the global environment, builds the request object, initializes the middleware pipeline, and emits the final response to the browser.
PHP
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use App\Http\Pipeline;
use App\Http\Middleware\ResponseTimeMiddleware;
use App\Http\Middleware\AuthenticationMiddleware;
use App\Http\RequestHandlerInterface;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
// 1. Capture the incoming HTTP request from server globals
$request = ServerRequestFactory::fromGlobals();
// 2. Define the core application handler (the innermost destination)
$routerFallback = new class implements RequestHandlerInterface {
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new JsonResponse([
'status' => 'success',
'data' => 'Welcome to the core application controller!'
]);
}
};
// 3. Assemble the Middleware Stack
$pipeline = new Pipeline($routerFallback);
$pipeline->pipe(new ResponseTimeMiddleware());
$pipeline->pipe(new AuthenticationMiddleware());
// 4. Dispatch the request through the stack
$response = $pipeline->handle($request);
// 5. Output the response to the client browser
$emitter = new SapiEmitter();
$emitter->emit($response);
Key Benefits of a Custom Middleware Pipeline
- Decoupled Business Logic: Cross-cutting concerns like logging, authentication, and security headers are kept entirely separate from core application controllers.
- Reusability: Individual middleware classes can be reused across different routes, APIs, or standalone projects.
- Short-Circuiting: Requests that fail security checks or rate limits are stopped at the outer layers before reaching expensive database queries or business operations.
- Standard Interoperability: By adhering to PSR-15/PSR-7 interfaces, custom middleware can be integrated into existing framework ecosystems like Slim or Mezzio.