Documentation menu

Middleware

php-via supports PSR-15 middleware for cross-cutting concerns like authentication, CORS, logging, and rate limiting. Middleware runs on page and action requests by default; implement SseAwareMiddleware to also run on SSE handshakes.

How it works

Middleware follows the standard PSR-15 onion model. Each middleware receives a PSR-7 ServerRequestInterface and can either short-circuit (return a response directly) or delegate to the next handler. php-via converts the OpenSwoole request to PSR-7 at the boundary and converts any PSR-7 response back, so the middleware layer is zero-overhead when no middleware is registered.

Global middleware

Global middleware runs on every page and action request. Register it with $app->middleware() before $app->start():

use Tuupola\Middleware\CorsMiddleware;

$app->middleware(new CorsMiddleware([
    'origin' => ['https://example.com'],
    'methods' => ['GET', 'POST'],
    'headers.allow' => ['Content-Type', 'Authorization'],
    'credentials' => true,
]));

Per-route middleware

Attach middleware to specific routes using the fluent API returned by $app->page():

$app->page('/admin', fn(Context $c) => $c->view('admin.html.twig'))
    ->middleware(new AuthMiddleware());

$app->page('/api/data', fn(Context $c) => $c->view('data.html.twig'))
    ->middleware(new AuthMiddleware(), new RateLimitMiddleware());

Per-route middleware runs after global middleware. The full pipeline is: global → route-specific → page handler.

Writing middleware

Implement Psr\Http\Server\MiddlewareInterface. Use $request->withAttribute() to pass data downstream — the attributes are automatically bridged to $context->getRequestAttribute() in your page handler.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Nyholm\Psr7\Response;

class AuthMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $sessionId = $this->extractSessionId($request);

        if (!$this->isAuthenticated($sessionId)) {
            // Short-circuit: return 302 redirect to login
            return new Response(302, ['Location' => '/login']);
        }

        // Pass user info downstream → available via $c->getRequestAttribute('user')
        return $handler->handle(
            $request->withAttribute('user', $this->getUser($sessionId))
        );
    }
}
Swoole warning: Middleware instances are long-lived — they persist across all requests in the worker process. Do not store per-request state on middleware properties. Use $request->withAttribute() to pass data downstream.

SSE-aware middleware

By default, middleware only runs on page and action requests — not on the SSE handshake. If you need middleware to also guard the SSE connection (e.g. auth), implement the SseAwareMiddleware marker interface:

use Mbolli\PhpVia\Http\Middleware\SseAwareMiddleware;

class AuthMiddleware implements SseAwareMiddleware
{
    // This now runs on page, action, AND SSE requests
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // ...
    }
}

Accessing middleware data in handlers

Middleware attributes set via $request->withAttribute() are automatically bridged to the Context:

$app->page('/dashboard', function (Context $c): void {
    $user = $c->getRequestAttribute('user');

    $c->view('dashboard.html.twig', [
        'name' => $user['name'],
    ]);
})->middleware(new AuthMiddleware());

Live example: Auth middleware

The Login Flow example demonstrates a working AuthMiddleware that protects the /examples/login/dashboard route. Unauthenticated users are redirected to the login form. Authenticated users see a personalised dashboard with their session data.

Built-in security

php-via includes several security features out of the box, independent of middleware:

CSRF protection

Actions validate the Origin header against a configurable allowlist. Configure with withTrustedOrigins():

$config = (new Config())
    ->withTrustedOrigins(['https://myapp.com', 'https://staging.myapp.com']);
  • null (default) — no restriction; all origins pass (development-friendly)
  • [] — block all browser-origin requests
  • ['https://example.com'] — strict allowlist

Secure session cookies

Enable withSecureCookie() in production to set the __Host- cookie prefix, which enforces HTTPS, Path=/, and no Domain attribute:

$config = (new Config())
    ->withSecureCookie(true);       // __Host-via_session_id; Secure; SameSite=Lax

Action rate limiting

Built-in per-IP sliding-window rate limiter for action endpoints:

$config = (new Config())
    ->withActionRateLimit(100, 60);  // max 100 actions per 60 seconds per IP

Proxy trust

If your app runs behind a reverse proxy (Caddy, nginx), enable proxy trust so X-Base-Path headers are honoured:

$config = (new Config())
    ->withTrustProxy(true);

Next steps

  • Actions — server-side event handlers
  • Lifecycle — connect, disconnect, intervals, cleanup
  • Deployment — production configuration