Přeskočit obsah

Middleware Pipeline a PSR-15

Detailnější průvodce middleware v Petrovo CMS a jak je psát.

Middleware Structure

Middleware v Petrovo CMS je pojmenovaná namespace funkce (App\Middlewares\*) se signaturou ($request, $handler): ResponseInterface:

// app/Middlewares/Log/loggingMiddleware.php
declare(strict_types=1);

namespace App\Middlewares;

use Psr\Http\Message\ResponseInterface;

function loggingMiddleware($request, $handler): ResponseInterface
{
    // Logika PŘED dalším middleware
    $method = $request->getMethod();
    $path = $request->getUri()->getPath();
    error_log("→ $method $path");

    // Zavolá další middleware v pipeline
    $response = $handler->handle($request);

    // Logika PO dalším middleware (úprava response)
    $response = $response->withHeader('X-Response-Time', (string) microtime(true));

    // Vrátí response zpět
    return $response;
}

Poznámky:

  • Middleware dostávají RequestHandlerInterface ($handler), ne prostou funkci. Voláš $handler->handle($request).
  • Návratový typ je vždy ResponseInterface (PSR-7 rozhraní), ne konkrétní HttpSoft\Message\Response$handler->handle() vrací rozhraní a middleware musí zůstat kompatibilní.
  • Každý middleware soubor se načítá přes composer files (viz composer.json) — tím se funkce zaregistruje globálně.
  • Historicky (Siler) byly middleware closury registrované přes define('x', $x). Dnes jsou to normální funkce.

Registrace do pipeline

Middleware se registruje přes pipe(). Protože jsou to pojmenované funkce, importuješ je a předáváš jako first-class callable middleware(...):

use function Core\Middleware\pipe;
use function App\Middlewares\{proxyMiddleware, csrfMiddleware, headersMiddleware};

pipe(proxyMiddleware(...), 'website');   // (...) vyrobí Closure z funkce, NEvolá ji
pipe(csrfMiddleware(...), 'website');
pipe(headersMiddleware(...), 'website');

Zápis middleware(...) nic nespustí — vytvoří callable, který pipe() uloží a zavolá teprve při zpracování requestu.

Middleware Pipelines

Petrovo CMS používá pojmenované pipeline pro různé typy požadavků:

Frontend Pipeline

Platí pro veřejné stránky (/ a /slug):

// app/routes/website.php
pipe(proxyMiddleware(...), 'website');
pipe(csrfMiddleware(...), 'website');
pipe(authMiddleware(...), 'website');
pipe(ipBlacklistMiddleware(...), 'website');
pipe(headersMiddleware(...), 'website');

Admin Pipeline

Platí pro /ADMIN a admin panel:

// app/routes/admin.php
pipe(proxyMiddleware(...), 'admin');
pipe(sessionExpirationAdminMiddleware(...), 'admin');
pipe(csrfAdminMiddleware(...), 'admin');
pipe(authAdminMiddleware(...), 'admin');
pipe(permissionAdminMiddleware(...), 'admin');
pipe(loggingAdminMiddleware(...), 'admin');
pipe(ipBlacklistMiddleware(...), 'admin');
pipe(headersMiddleware(...), 'admin');

Admin Tools Pipeline

Lehčí pro soubory a utility:

pipe(proxyMiddleware(...), 'adminTools');
pipe(sessionExpirationAdminMiddleware(...), 'adminTools');
pipe(authAdminMiddleware(...), 'adminTools');
pipe(permissionAdminMiddleware(...), 'adminTools');
pipe(loggingAdminMiddleware(...), 'adminTools');
pipe(ipBlacklistMiddleware(...), 'adminTools');

RUN Pipeline

Pro dynamické tagy a akce (/RUN):

// app/routes/run.php
pipe(proxyMiddleware(...), 'run');
pipe(csrfMiddleware(...), 'run');
pipe(corsMiddleware(...), 'run');
pipe(ipBlacklistMiddleware(...), 'run');

RUN admin pipeline

pipe(proxyMiddleware(...), 'run-admin');
pipe(sessionExpirationAdminMiddleware(...), 'run-admin');
pipe(csrfAdminMiddleware(...), 'run-admin');
pipe(authAdminMiddleware(...), 'run-admin');
pipe(corsMiddleware(...), 'run-admin');
pipe(ipBlacklistMiddleware(...), 'run-admin');

Psaní vlastního middleware

Každý příklad je soubor v app/Middlewares/, namespace App\Middlewares;, a musí být uveden v composer.json v poli files, aby se funkce načetla.

Jednoduché middleware - logging

// app/Middlewares/Log/loggingMiddleware.php
namespace App\Middlewares;

use Psr\Http\Message\ResponseInterface;

function loggingMiddleware($request, $handler): ResponseInterface
{
    $method = $request->getMethod();
    $uri = $request->getUri();

    error_log("Request: $method $uri");

    $response = $handler->handle($request);

    error_log("Response: " . $response->getStatusCode());

    return $response;
}

Middleware s podmínkou - autentifikace

// app/Middlewares/Auth/authMiddleware.php
namespace App\Middlewares;

use Psr\Http\Message\ResponseInterface;

use function Core\Http\text;

function authMiddleware($request, $handler): ResponseInterface
{
    // Extrahuj token z Authorization headeru
    $authHeader = $request->getHeaderLine('Authorization');

    if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
        return text('Unauthorized', 401);
    }

    $token = substr($authHeader, 7);  // Remove 'Bearer '

    if (!validateToken($token)) {
        return text('Invalid token', 401);
    }

    // Autentifikace OK - pokračuj do dalšího middleware/handleru
    return $handler->handle($request);
}

Middleware s atributy - přidá data do request

// app/Middlewares/Session/userMiddleware.php
namespace App\Middlewares;

use Psr\Http\Message\ResponseInterface;

function userMiddleware($request, $handler): ResponseInterface
{
    $userId = Session::get('user_id');

    if ($userId) {
        $user = DB::row("SELECT * FROM users WHERE id = ?", [$userId]);
        // Přidá atribut do request (pro další middleware)
        $request = $request->withAttribute('user', $user);
    }

    return $handler->handle($request);
}

Middleware s úpravou response - přidá header

// app/Middlewares/Response/securityHeadersMiddleware.php
namespace App\Middlewares;

use Psr\Http\Message\ResponseInterface;

function securityHeadersMiddleware($request, $handler): ResponseInterface
{
    $response = $handler->handle($request);

    return $response
        ->withHeader('X-Content-Type-Options', 'nosniff')
        ->withHeader('X-Frame-Options', 'SAMEORIGIN')
        ->withHeader('X-XSS-Protection', '1; mode=block');
}

Pipeline execution

Middleware se spouští v pořadí registrace:

Request
[Middleware 1] START
  [Middleware 2] START
    [Middleware 3] START
      [Handler] - vrátí Response
    [Middleware 3] END - upraví response
[Middleware 2] END - upraví response
[Middleware 1] END - upraví response
Response

Konkreetní příklad: Frontend request

GET /some-page
proxyMiddleware - Detekuje proxy, nastaví remote IP
csrfMiddleware - Ověří CSRF token
authMiddleware - Načte uživatele / auth kontext
ipBlacklistMiddleware - Zablokuje blacklistované IP
headersMiddleware - Nastaví response headers
app/website/slug.php (Handler)
Response (s headers, body, status)
Browser

Důležité notes

  1. Pořadí je kritické - middleware se spouští v pořadí registrace
  2. Chyby v middleware - pokud middleware vyhodí výjimku, pipeline se zastaví
  3. Request je immutable - PSR-7 objekty jsou immutable, musíš usar withAttribute(), withHeader(), atd.
  4. Handler je volitelný - middleware může vrátit response bez volání $handler->handle($request)
  5. Response objekty - vždy vrať validní Response objekt (nebo exit)

Kdy psát middleware?

Middleware je vhodný pro:

  • Globální validace/sanitizace
  • Autentifikaci a autorizaci
  • Setování headers (CORS, security headers)
  • Requestu modifikace (přidání atributů)
  • Loggování/monitorování

Middleware NENÍ vhodný pro:

  • Business logiku (je to v Controller)
  • Databázový query (je to v Model/Endpoint)
  • Template rendering (je to v Template layer)