SPIN Framework uses a middleware system that allows you to intercept and modify HTTP requests and responses. Middleware extends the Spin\Core\Middleware class and provides a clean way to handle cross-cutting concerns like authentication, logging, and CORS.
SPIN middleware classes extend Spin\Core\Middleware and implement two main methods:
<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
class ExampleMiddleware extends Middleware
{
/**
* Initialize the middleware
*
* @param array $args The arguments
* @return bool
*/
public function initialize(array $args): bool
{
// Setup code here
return true;
}
/**
* Handle the request/response
*
* @param array $args URI parameters as key=value array
* @return bool True=OK, False=Failed to handle it
*/
public function handle(array $args): bool
{
// Middleware logic here
return true;
}
}Called once when the middleware is created. Use this method for:
- Loading configuration
- Setting up dependencies
- Initializing resources
Return true if initialization succeeds, false if it fails.
Called for each request. Use this method for:
- Processing the request
- Modifying the response
- Performing validation
Return true if the request should continue, false if it should be blocked.
SPIN provides several helper functions that middleware can use:
// Get configuration values
$secret = config('application.secret');
$timeout = config('session.timeout');
$logLevel = config('logger.level', 'info');// Store values in the container
container('user', $user);
container('requestId', $requestId);
// Retrieve values from the container
$user = container('user');
$requestId = container('requestId');// Get the current request
$request = getRequest();
// Get the current response
$response = getResponse();
// Create a new response
return response($content, $statusCode, $headers);// Log messages
logger()->info('User authenticated', ['userId' => $userId]);
logger()->error('Authentication failed', ['error' => $error]);
logger()->critical('Critical error', ['trace' => $trace]);<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
use Spin\Helpers\JWT;
class AuthHttpBeforeMiddleware extends Middleware
{
/** @var string Secret string */
protected $secret;
/**
* Initialize
*/
public function initialize(array $args): bool
{
# Get applications global secret
$this->secret = config('application.secret');
return true;
}
/**
* Handle authentication
*/
public function handle(array $args): bool
{
$authenticated = false;
$type = 'token';
$token = config('integrations.core.token');
$authenticated = $this->authToken($token);
# Failed authentication
if (!$authenticated && getResponse()->getStatusCode() < 400) {
response('', 401, [
'WWW-Authenticate' => $type . ' realm="' .
(getRequest()->getHeader('Host')[0] ?? '') . '"'
]);
}
return $authenticated;
}
/**
* Token authentication
*/
protected function authToken(string $token): bool
{
$authenticated = false;
$tokens = config('tokens') ?? [];
$authenticated = array_key_exists($token, $tokens);
return $authenticated;
}
/**
* Bearer authentication (JWT)
*/
protected function authBearer(string $token): bool
{
$authenticated = false;
try {
# Verify the Token and decode the payload
$payload = JWT::decode($token, $this->secret, ['HS256']);
if (!is_null($payload)) {
# Store the Payload in the Dependency Container
container('jwt:payload', $payload);
$authenticated = true;
}
} catch (\Exception $e) {
logger()->critical($e->getMessage(), [
'rid' => container('requestId'),
'msg' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
return $authenticated;
}
}<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
class SessionBeforeMiddleware extends Middleware
{
/**
* Retrieve or Set the SessionId cookie for the request
*/
public function handle(array $args): bool
{
# Build session array
$session = [];
$session['cookie'] = config('session.cookie');
$session['timeout'] = config('session.timeout');
$session['refresh'] = config('session.refresh');
if (!empty($session['cookie'])) {
$session['value'] = cookie($session['cookie']);
} else {
$session['value'] = '';
}
# Set array to container
container('session', $session);
return true;
}
}<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
class CorsBeforeMiddleware extends Middleware
{
/**
* Handle CORS preflight and add CORS headers
*/
public function handle(array $args): bool
{
$request = getRequest();
$response = getResponse();
# Handle preflight OPTIONS request
if ($request->getMethod() === 'OPTIONS') {
$response = $response->withStatus(200);
$response = $response->withHeader('Access-Control-Allow-Origin', '*');
$response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response = $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return true;
}
# Add CORS headers to all responses
$response = $response->withHeader('Access-Control-Allow-Origin', '*');
$response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response = $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return true;
}
}<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
class RequestIdBeforeMiddleware extends Middleware
{
/**
* Generate and set request ID
*/
public function handle(array $args): bool
{
$requestId = uniqid('req_', true);
container('requestId', $requestId);
# Add request ID to response headers
$response = getResponse();
$response = $response->withHeader('X-Request-ID', $requestId);
return true;
}
}<?php declare(strict_types=1);
namespace App\Middlewares;
use Spin\Core\Middleware;
class ResponseLogAfterMiddleware extends Middleware
{
/**
* Log response information
*/
public function handle(array $args): bool
{
$response = getResponse();
$request = getRequest();
$requestId = container('requestId');
logger()->info('Response completed', [
'rid' => $requestId,
'method' => $request->getMethod(),
'path' => $request->getUri()->getPath(),
'status' => $response->getStatusCode(),
'size' => $response->getBody()->getSize()
]);
return true;
}
}Middleware is registered in the route configuration file (routes-dev.json):
Applied to all routes:
{
"common": {
"before": [
"\\App\\Middlewares\\RequestIdBeforeMiddleware"
],
"after": [
"\\App\\Middlewares\\ResponseTimeAfterMiddleware",
"\\App\\Middlewares\\ResponseLogAfterMiddleware"
]
}
}Applied to specific route groups:
{
"name": "Private",
"prefix": "/api/v1",
"before": [
"\\App\\Middlewares\\AuthHttpBeforeMiddleware"
],
"routes": [
{ "methods": ["GET"], "path": "/profile", "handler": "\\App\\Controllers\\ProfileController" }
]
}Applied to individual routes:
{
"methods": ["POST"],
"path": "/users",
"handler": "\\App\\Controllers\\UserController",
"middleware": [
"\\App\\Middlewares\\ValidationMiddleware"
]
}Middleware execution order is important:
- Common Before - Applied to all routes
- Group Before - Applied to route groups
- Route Before - Applied to specific routes
- Controller - Route handler execution
- Route After - Applied to specific routes
- Group After - Applied to route groups
- Common After - Applied to all routes
- Single Responsibility: Each middleware should handle one concern
- Performance: Keep middleware lightweight and efficient
- Error Handling: Always handle exceptions gracefully
- Logging: Log important events and errors
- Configuration: Use configuration for customizable behavior
- Testing: Write unit tests for middleware logic
- Documentation: Document middleware purpose and behavior
Middleware can be conditionally applied based on request properties:
public function handle(array $args): bool
{
$request = getRequest();
# Only apply to API routes
if (!str_starts_with($request->getUri()->getPath(), '/api/')) {
return true;
}
# Apply middleware logic
return $this->processApiRequest($args);
}Middleware can receive parameters from the route configuration:
{
"before": [
"\\App\\Middlewares\\RateLimitMiddleware:100,60"
]
}public function initialize(array $args): bool
{
# Parse parameters (e.g., "100,60" -> max 100 requests per 60 seconds)
$params = explode(',', $args['params'] ?? '100,60');
$this->maxRequests = (int)($params[0] ?? 100);
$this->timeWindow = (int)($params[1] ?? 60);
return true;
}Always handle errors gracefully in middleware:
public function handle(array $args): bool
{
try {
// Middleware logic
return $this->processRequest($args);
} catch (\Exception $e) {
logger()->error('Middleware error: ' . $e->getMessage(), [
'rid' => container('requestId'),
'middleware' => static::class,
'error' => $e->getMessage()
]);
// Return false to block the request or true to continue
return false;
}
}Test middleware in isolation:
<?php
// tests/Middleware/AuthMiddlewareTest.php
use PHPUnit\Framework\TestCase;
class AuthMiddlewareTest extends TestCase
{
public function testSuccessfulAuthentication()
{
$middleware = new AuthHttpBeforeMiddleware();
$args = ['token' => 'valid_token'];
$result = $middleware->handle($args);
$this->assertTrue($result);
}
public function testFailedAuthentication()
{
$middleware = new AuthHttpBeforeMiddleware();
$args = ['token' => 'invalid_token'];
$result = $middleware->handle($args);
$this->assertFalse($result);
}
}This middleware system provides a powerful and flexible way to handle cross-cutting concerns in your SPIN application while maintaining clean separation of concerns.