Skip to content

PSR-7 a PSR-15: HTTP Messages a Middleware

Petrovo CMS implementuje standardy PSR-7 (HTTP Message Interface) a PSR-15 (HTTP Server Request Handler) prostřednictvím:

  • HttpSoft - PSR-7 implementace (Request/Response objekty)
  • Core\Http - Funkcionální API pro PSR-7 (Request + Response helpery)
  • Core\Middleware - Lightweight PSR-15 pipeline s pojmenovanými kanály
  • Core\Route - Registry-based router s automatickou middleware integrací

Jde o custom lightweight stack bez Laminas/Siler overhead — čistě funkcionální design optimalizovaný pro malé týmy a vysokou čitelnost.

PSR-7: HTTP Messages

PSR-7 definuje standardní objekty pro práci s HTTP requesty a responses.

Request vs. Response

Request (ServerRequest):

  • Reprezentuje příchozí HTTP požadavek
  • Obsahuje: URL, metodu (GET/POST), headers, body, query parametry
  • Vytvoří se z superglobálních proměnných ($_GET, $_POST, atd.)

Response:

  • Reprezentuje odchozí HTTP odpověď
  • Obsahuje: status code, headers, body
  • V Petrovo CMS se vytváří přes helpery (html(), json(), atd.) a odesílá přes emit()

Vytvoření Request objektu

use function Core\Http\request;

// Vytvoří ServerRequestInterface ze superglobálních proměnných
$request = request();

Interně zpracuje:

$_GET       // Query parametry
$_POST      // Parsed body
$_SERVER    // Server params + HTTP headers
$_COOKIE    // Cookies
$_FILES     // Konvertováno na UploadedFileInterface objekty

Proxy support: Detekuje reverse proxy (nginx, Caddy, Cloudflare) a správně extrahuje IP a hostname z X-Forwarded-* headerů.

Request helpery

use function Core\Http\{
    request, path, method, query, body, field,
    with_attribute, get_attribute, attributes,
    header, has_header, raw, is_json, parse_json,
    authorization_header, bearer, raw2json,
    get_host_from_request
};

$request = request();

path($request);                          // '/some-page'
method($request);                        // 'GET', 'POST', ...

query($request, 'id');                   // $_GET['id']
query($request, 'page', 1);             // s výchozí hodnotou

body($request);                          // $_POST (array|object|null)
field($request, 'name');                 // $_POST['name']
field($request, 'email', '');           // s výchozí hodnotou

header($request, 'Content-Type');        // hodnota headeru
has_header($request, 'Authorization');   // bool

raw($request);                           // raw php://input
is_json($request);                       // bool (Content-Type: application/json)
parse_json($request);                    // decoded JSON body
raw2json($request);                      // JSON body pokud je JSON, jinak null

authorization_header($request);          // 'Bearer xyz...' | null
bearer($request);                        // 'xyz...' | null (extrahuje token)

get_host_from_request();                 // hostname s proxy podporou

Atributy (komunikace mezi middleware):

$request = with_attribute($request, 'user_id', 42);
$userId = get_attribute($request, 'user_id');
$all = attributes($request);

Response helpery

use function Core\Http\{
    html, json, translatedJson, text, binary,
    redirect, no_content,
    with_header, with_headers, with_status,
    emit
};

// HTML response
html('<p>Obsah</p>');
html('<p>Chyba</p>', 500);

// JSON response
json(['data' => 'value']);
json(['error' => 'Not found'], 404);

// Přeložená JSON response (i18n)
translatedJson(true, 'save_success');           // {status: 'ok', message: '...'}
translatedJson(false, 'validation_error', [], 'cs', 422);

// Textová response
text('Plain text');

// Binární response (obrázky, soubory)
binary($imageData, 'image/jpeg');
binary($pdfData, 'application/pdf');

// Redirect
redirect('/new-url');
redirect('/gone', 301);

// Empty response (204 No Content)
no_content();

// Modifikátory (immutable — vrátí nový objekt)
$response = with_header($response, 'X-Custom', 'value');
$response = with_headers($response, ['X-A' => '1', 'X-B' => '2']);
$response = with_status($response, 201);

// Odeslání klientovi
emit($response);

Ostatní Http helpery

use function Core\Http\{isSecure, HTTPStatus};

isSecure();          // bool — detekuje HTTPS včetně proxy headerů
HTTPStatus(404);     // ['code' => 404, 'error' => 'HTTP/1.1 404 Not Found']
                     // + pošle header()

PSR-15: Middleware Pipeline

PSR-15 definuje standardní middleware interface. Middleware je vrstva, která:

  1. Obdrží request
  2. Provede nějakou logiku (autentifikace, validace, atd.)
  3. Předá request dalšímu middleware v řetězu
  4. Přijme response zpět a provede finální úpravy

Middleware v Petrovo

Middleware v Petrovo jsou funkce/closures, které přijímají:

  • $request - PSR-7 ServerRequestInterface
  • $handler - RequestHandlerInterface pro předání dalšímu middleware
  • Vrací: PSR-7 ResponseInterface
$middleware = function($request, $handler) {
    // 1. Logika PŘED dalším middleware
    error_log("Request: " . $request->getMethod() . " " . $request->getUri()->getPath());

    // 2. Předá request dál
    $response = $handler->handle($request);

    // 3. Logika PO dalším middleware (úprava response)
    $response = $response->withHeader('X-Custom-Header', 'value');

    // 4. Vrátí response
    return $response;
};

Registrace middleware — pipe()

Middleware se registrují do pojmenované pipeline pomocí pipe():

use function Core\Middleware\pipe;

pipe($middleware1, 'website');
pipe($middleware2, 'website');
pipe($middleware3, 'website');

Pořadí je důležité — middleware se spouští v pořadí registrace. Thread-safe: Používá statickou storage namísto globálů — funguje v FrankenPHP, Swoole, RoadRunner.

Spuštění pipeline — process() a handle()

process() — Wrappuje middleware okolo route handleru

use function Core\Middleware\process;

$handler = fn($request) => html('<p>OK</p>');
$middlewareHandler = process($request, 'website')($handler);
$response = $middlewareHandler->handle($request);

process() vrací closure, která přijme $handler a vrátí RequestHandlerInterface.

handle() — Spustí middleware pipeline bez handleru

use function Core\Middleware\handle;

$response = handle($request, 'website');

Všechna middleware se spustí a vrátí finální response.

Utility funkce

use function Core\Middleware\{wrap_handler, reset, get_pipelines};

// Převede RequestHandlerInterface na callable (pro routing)
$callable = wrap_handler($handler);

// Vymaže všechny pipelines (pro testování)
reset();

// Vrátí registrované pipelines (pro debugging)
$pipelines = get_pipelines();

Výstup — emit()

use function Core\Http\emit;

emit($response);

Odešle status code, headers a body klientovi.

Architekt Petrovo — Custom PSR Stack

Petrovo CMS nahradil Laminas/Siler vlastní lightweight implementací:

┌────────────────────────────────────────┐
│ App Bootstrap (app/bootstrap.php)      │
│ └─ Path-based dispatcher               │
│    (podle /getData/, /ADMIN, atd.)     │
└────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────┐
│ Route Files (app/routes/*.php)         │
│ ├─ Zaregistrují middleware             │
│ │  (pipe → Core\Middleware)         │
│ ├─ Zaregistrují routes                 │
│ │  (get/post/any → Core\Route)      │
│ └─ Zavolají dispatch()                 │
└────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────┐
│ Route Dispatcher (core/Route/Route.php)│
│ ├─ Matchuje patterns                   │
│ ├─ Extrahuje parametry                 │
│ └─ Aplikuje middleware pipeline        │
└────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────┐
│ Middleware Pipeline (core/Middleware)  │
│ ├─ Named pipelines (frontend/admin)    │
│ └─ Spouští middleware v pořadí         │
└────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────┐
│ Handler (Controller/Tag/Script)        │
│ └─ Vrátí PSR-7 Response                │
└────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────┐
│ HTTP Response (core/Http/Response.php) │
│ └─ emit() → Output na klient           │
└────────────────────────────────────────┘

Tři typy Route Handleru

Petrovo Route dispatcher podporuje tři typy handleru — file paths, closures a class methods:

1. File Path Handler

use function Core\Route\get;

get('/{slug}', __DIR__ . '/../Controllers/slug.php', 'website');

V Controllers/slug.php:

<?php
// Route params dostupné jako $params array i jako proměnné ($slug, atd.)
$params = $request->getAttributes();
return html("<h1>$slug</h1>");

2. Closure Handler

use function Core\Route\post;

post('/search', function($request, $params) {
    $query = $params['query'] ?? null;
    $results = searchDatabase($query);
    return json($results);
}, 'api');

3. Array Notation — [ClassName::class, 'method']

use function Core\Route\{get, post, put, delete};
use App\Modules\Admins\Controller;

get('/ADMIN/admins',        [Controller::class, 'index'],  'admin');
post('/ADMIN/admins',       [Controller::class, 'store'],  'admin');
put('/ADMIN/admins/{id}',   [Controller::class, 'update'], 'admin');
delete('/ADMIN/admins/{id}',[Controller::class, 'destroy'],'admin');

V Controller:

class Controller {
    public function index($request, $params): Response { ... }
    public function store($request, $params): Response { ... }
    public function update($request, $params): Response { ... }
    public function destroy($request, $params): Response { ... }
}

Výhody array notation:

  • IDE autocomplete a static analysis (PHPStan)
  • Explicitní a čitelný kód
  • Automatická instanciace třídy

Registrace HTTP metod

use function Core\Route\{get, post, put, patch, delete, any};

get('/path', $handler, 'pipeline');
post('/path', $handler, 'pipeline');
put('/path/{id}', $handler, 'pipeline');
patch('/path/{id}', $handler, 'pipeline');
delete('/path/{id}', $handler, 'pipeline');
any('/path', $handler, 'pipeline');      // Matchuje všechny metody

Route patterns

/exact/path       - přesná shoda
/path/{id}        - povinný parametr (non-empty)
/path/{id}?       - volitelný parametr (může být prázdný)
/prefix/.*        - wildcard prefix (matchuje vše od /prefix/)

Utility funkce

use function Core\Route\{dispatch, reset, routes, matched_route};

$response = dispatch($request);   // Matchuje routes a spustí handler

reset();                          // Vymaže routes (pro testování)
$all = routes();                  // Vrátí registrované routes (pro debugging)
$matched = matched_route();       // Vrátí naposledy matchovanou route (pro Tracy panel)

Praktický flow — Request Processing

1. Frontend Page Request

GET /some-page
   ↓ app/bootstrap.php (path-based dispatcher)
   └─> /some-page → require 'website.php'
   ↓ app/routes/website.php
   ├─ pipe() zaregistruje middleware
   ├─ any('/{slug}', controller, 'frontend') zaregistruje route
   └─ dispatch(request())
   ↓ core/Route/Route.php: dispatch()
   ├─ Matchuje pattern: /{slug}
   ├─ Extrahuje: slug = 'some-page'
   └─ Aplikuje middleware: process($request, 'frontend')($handler)
   ↓ core/Middleware/Pipeline.php
   ├─ Spouští middleware v pořadí:
   │  1. proxyMiddleware
   │  2. defineWebMiddleware
   │  3. csrfMiddleware
   │  4. headersMiddleware
   └─ Zavolá handler
   ↓ Controller/slug.php
   └─ return html("<h1>$slug</h1>")
   ↓ Response zpět middleware stackem → emit()

2. Admin Controller Request

GET /ADMIN/admins
   ↓ app/routes/admin.php
   ├─ pipe() zaregistruje admin middleware
   ├─ get('/ADMIN/admins', [Controller::class, 'index'], 'admin')
   └─ dispatch(request())
   ↓ dispatch() → instanciuje Controller, zavolá index($request, $params)
   ↓ Middleware: auth, session, permissions
   ↓ Controller::index() → return json([...])
   ↓ emit($response)

Architekturní Srovnání

Aspekt OLD (Laminas/Siler) NEW (Petrovo)
Request Siler\Diactoros\request() Core\Http\request()
Response Siler\Diactoros helpers Core\Http{html, json, binary, ...}
Middleware Siler\Stratigility\pipe() Core\Middleware\pipe()
Pipeline Siler\Stratigility\process() Core\Middleware\process()
Routing Siler\Route Core\Route
Emit HttpHandlerRunner\sapi_emit() Core\Http\emit()
Handler Types File path, Closure File path, Closure, [Class::method]
HTTP Methods get, post, any get, post, put, patch, delete, any

Shrnutí — Petrovo Custom Stack

Core\Http (core/Http/):

  • request() - PSR-7 ServerRequest z globals
  • Request helpery: path(), method(), query(), body(), field(), header(), bearer(), ...
  • Response helpery: html(), json(), translatedJson(), text(), binary(), redirect(), no_content()
  • Response modifikátory: with_header(), with_headers(), with_status()
  • emit() - Send response to client
  • isSecure(), HTTPStatus() - HTTP utility funkce

Core\Middleware (core/Middleware/Pipeline.php):

  • pipe() - Registrace middleware do named pipeline
  • process() - Wrap middleware okolo handleru
  • handle() - Spuštění pipeline bez handleru
  • wrap_handler() - Převod RequestHandlerInterface na callable
  • reset(), get_pipelines() - Utility (testing, debugging)

Core\Route (core/Route/Route.php):

  • get(), post(), put(), patch(), delete(), any() - Registrace routes
  • dispatch() - Match routes a spuštění s middleware
  • Tři handler types: file path, closure, [Class::method]
  • Pattern matching s cachováním
  • matched_route(), reset(), routes() - Utility (debugging, testing)

Výsledek: Lightweight, funkcionální, bez Laminas/Siler overhead

  • Vyšší výkon (žádné zabalování)
  • Lepší čitelnost (vždy vidíš co se děje)
  • Menší složitost (méně vrstvení)
  • Ideální pro malé týmy s full codebase knowledge