Middleware
Triangle Middleware builds the HTTP chain around a controller. An interceptor can validate the request, return its own response or call the next handler and modify the result. This is a Triangle contract, not PSR-15: process receives a request and a callable.
Installation
Section titled “Installation”composer require triangle/middlewareRequirements: PHP ^8.1, triangle/engine ^8.1 and triangle/router ^2.0. Engine-HTTP executes the chain; this component stores registrations and assembles callbacks.
Implementing middleware
Section titled “Implementing middleware”<?php
namespace app\middleware;
use Triangle\Middleware\MiddlewareInterface;
final class ResponseHeader implements MiddlewareInterface{ public function process($request, callable $handler) { $response = $handler($request); return $response->header('X-Application', 'example'); }}Perform checks before handler and response processing after it. Return a Response without calling handler to stop the request. Do not store the current request or user on a shared middleware instance: the container and worker determine instance lifetime.
Registration
Section titled “Registration”Middleware.php maps scopes to class lists:
return ['@' => [app\middleware\ResponseHeader::class]];The @ scope is global. An empty string is the common scope for the current application/plugin, and a named key selects an app. Static.middleware is loaded separately under static.
Load checks method_exists(process), not interface implementation, method visibility or signature compatibility. Implement MiddlewareInterface and verify the HTTP route instead of treating successful registration as proof of execution.
Route and controller scopes
Section titled “Route and controller scopes”RouteObject can supply middleware. Controllers can declare a middleware property, class attributes and method attributes using Triangle Annotation Middleware. The component combines these when preparing the callback; it does not execute them during config loading.
Config lists preserve their declaration order on entry into the HTTP chain and unwind in reverse after handler. Global surrounds app-global, then app and controller/route processing. GetMiddleware returns a reversed list for Engine’s array_reduce wrapping; do not execute that list with a plain foreach.
Route middleware is also reversed separately during collection. When combining route, property and attribute sources, assert the expected execution trace rather than assuming every list shares the same ordering rule.
Errors and reloads
Section titled “Errors and reloads”Invalid list types or missing process methods are rejected during load. Repeated load appends registrations and can duplicate execution; it does not replace the previous configuration. Start(null) does nothing.
Engine-HTTP catches Throwable from process and delegates to the application’s exception response handler. It does not retry the operation. Side effects need application-level idempotency and explicit error handling.
Interface
Section titled “Interface”MiddlewareInterface::process($request, callable $handler) has no declared return type. An HTTP chain expects a compatible Response result. The handler is called only if processing should continue. The two-argument HTTP contract is not interchangeable with a different WebSocket interceptor signature.
Bootstrap
Section titled “Bootstrap”| Method | Contract |
|---|---|
| start(?Server $server = null) | Load app/static/plugin configuration; null does nothing |
| load(array $list, string $plugin = ”) | Append scoped registrations |
| getMiddleware(plugin, app, controller, route, withGlobal = true) | Return callback pairs for wrapping the final handler |
Load stores [class, 'process']. Engine-HTTP resolves classes with container->get. WithGlobal=false excludes global and app-global, but not the named app or route/controller scopes.
Scope keys: @ is global, empty string is app/plugin-common, an app name is app-specific, static is static resources, and plugin.{plugin}.{app} explicitly selects a plugin/app with the default alias.
Encountering @ resets the loader’s local plugin variable for subsequent entries in that load call. Avoid mixing @ with implicit plugin-scoped entries; use explicit scopes and verify the assembled list.
Ordering and reflection
Section titled “Ordering and reflection”GetMiddleware reverses the merged global/app-global/app/controller/route array. Engine-HTTP wraps it with array_reduce, so the first declared global runs outermost. RouteObject middleware is separately reversed before merging.
Controller reflection applies when the controller is an array with a class-name string as its first element. An already-created object in that array does not take the same reflection branch.
Install copies the bundled middleware.php only when missing. The default contains an empty list for the empty-string scope. Autoloading a middleware class does not register it.