Skip to content

Migrace: Siler + Laminas → Petrovo Router

Dokumentace změn z původního stacku (Siler + Laminas) na nový lightweight router (Petrovo\Route + Petrovo\Middleware).


Přehled Změn

Co se změnilo

Aspekt Staré (Siler/Laminas) Nové (Petrovo)
Router Siler\Route Petrovo\Route
Middleware Siler\Stratigility (Laminas) Petrovo\Middleware
HTTP Siler\Diactoros Petrovo\Http
Handler wrapper require_fn() + process() Přímé funkce / soubory
Routing pattern Všechny routes globálně Path-based pre-dispatch
Emit HttpHandlerRunner\sapi_emit() Petrovo\Http\emit()
PSR-7 Siler wrapper Native (HttpSoft)

Filosofie

Změna z konvence a abstrakce na determinismus a transparentnost:

  • Víš přesně, co se děje (bez skrytých volání)
  • Bez černého boxu Laminas/Siler
  • Direktní SQL, viditelné middleware, explicitní routing
  • Menší overhead = vyšší výkon
  • Vhodné pro malé týmy, které znají celý codebase

1. Bootstrap: Path-Based Pre-Dispatch

Staré řešení (develop)

// app/bootstrap.php (OLD)

// Všechny route soubory se require-ují automaticky
require 'routes/data.php';
require 'routes/file.php';
require 'routes/run.php';
require 'routes/cron.php';
require 'routes/app-backend.php';
require 'routes/app-frontend.php';
require 'routes/404.php';

// Všechny route soubory si registry vytvářejí a dispatch se volá v každém
// Výsledek: všechny routes se registrují, pak se hledá matchem

Problém: Všechny route soubory se vždy loadují, všechny se registrují, pak se matchuje. Neefektivní.

Nové řešení (feature/new-routing)

// app/bootstrap.php (NEW)

use function Petrovo\Http\{emit, html, path, request};

// === PRE-ROUTING: Fast path-based dispatcher ===
// Podle prefixu URL routuje na správný soubor
// Např: /getData/* → routes/data.php
//       /ADMIN    → routes/app-backend.php
//       /        → routes/app-frontend.php

$request = request();
$requestPath = path($request);

// Early security check (static files, technical endpoints)
$blockingResponse = require 'blocking.php';
if ($blockingResponse) {
    emit($blockingResponse);
    exit;
}

// Path-based dispatcher - zvolí správný route soubor
$response = null;

if (str_starts_with($requestPath, '/getData/')) {
    $response = require 'routes/data.php';
} elseif (str_starts_with($requestPath, '/getFile/')) {
    $response = require 'routes/file.php';
} elseif (str_contains($requestPath, '/RUN/')) {
    $response = require 'routes/run.php';
} elseif (str_starts_with($requestPath, '/CRON/')) {
    $response = require 'routes/cron.php';
} elseif (str_starts_with($requestPath, '/ADMIN')) {
    $response = require 'routes/app-backend.php';
} else {
    $response = require 'routes/app-frontend.php';
}

// Fallback
if (!$response) {
    $response = html($_SERVER['REQUEST_URI'] . " — not found", 404);
}

emit($response);
exit;

Výhody:

  • ✅ Loadují se jen relevantní routes
  • ✅ Rychlejší startup
  • ✅ Jasné a explicitní (vidíš přesně, co se děje)
  • ✅ Fyzická separace kódu dle typu requestu

2. Route Registrace: Od Siler→ Petrovo

Staré řešení: Siler\Route (develop)

// app/routes/app-frontend.php (OLD)

use Siler\Diactoros;
use Siler\Route;
use function Siler\require_fn;
use function Siler\Stratigility\pipe;
use function Siler\Stratigility\process;

$request = Diactoros\request();

// Middleware
pipe(proxyMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');

// Routes - komplikované wrappování
$slugHandler = fn($request, $params) => require_fn(
    __DIR__ . '/../Controllers/slug.php'
)($params);

Route\any('/{slug}?', process($request, 'frontend')($slugHandler), $request);

Problém: Siler\Route je komplikovaný, require_fn() wrapper je nepřehledný, process() se volá manuálně.

Nové řešení: Petrovo\Route (feature/new-routing)

// app/routes/app-frontend.php (NEW)

use function Petrovo\Http\{html, request};
use function Petrovo\Middleware\pipe;
use function Petrovo\Route\{any, get, dispatch};

// Middleware
pipe(proxyMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');
pipe(csrfMiddleware, 'frontend');
pipe(settingMiddleware, 'frontend');
pipe(headersMiddleware, 'frontend');

// Routes - přímé a čitelné
get('/newsletter-email-activation', __DIR__ . '/../tags/newsletter/add-email/activation.php', 'frontend');
any('/', __DIR__ . '/../Controllers/slug.php', 'frontend');
any('/{slug}', __DIR__ . '/../Controllers/slug.php', 'frontend');

// Dispatch - automaticky aplikuje middleware
$response = dispatch(request());

// Fallback 404
if (!$response) {
    $response = html(show404(), 404);
}

return $response;

Výhody:

  • ✅ Čitelný kód - vidíš, co se registruje
  • ✅ Bezprostřední - bez require_fn(), bez process()
  • ✅ Middleware se aplikuje automaticky - jenom zaregistruješ
  • ✅ dispatch() vrací Response - nikdy se neexitu v route souboru

3. Handler Typy: Tři Varianty

Nový router podporuje tři typy handleru:

1. File Path Handler (Nejčastěji)

Handlerem je cesta k PHP souboru - bude vyžadován s dostupnými parametry:

use function Petrovo\Route\get;

// Handler = cesta k souboru
get('/{slug}', __DIR__ . '/../Controllers/slug.php', 'frontend');

// V Controllers/slug.php se automaticky dostupné:
// - $slug    ← extrahovaný z {slug}
// - $params  ← ['slug' => 'homepage']
// - $request ← PSR-7 ServerRequestInterface

V kontroleru:

// Controllers/slug.php
<?php
declare(strict_types=1);

use function Petrovo\Http\html;

// $slug je dostupný z route parametru
// $request je PSR-7 request
// $params je ['slug' => 'homepage']

$page = DB::row("SELECT * FROM pages WHERE slug = ?", [$slug]);

return html("<h1>{$page->title}</h1>");

2. Closure Handler (Inline)

Handlerem je inline funkce s přístupem k $request a $params:

use function Petrovo\Route\post;
use function Petrovo\Http\json;

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

3. Array Notation: [Class::method] - NOVÝ

Handlerem je [ClassName::class, 'methodName'] - třída se automaticky instancuje:

use function Petrovo\Route\get;
use App\Admin\Controllers\AdminsController;

// Handler = [Class::method]
get('/ADMIN/admins', [AdminsController::class, 'index'], 'admin');
post('/ADMIN/admins', [AdminsController::class, 'store'], 'admin');

// V AdminsController se volá:
// - $this->index($request, $params)
// - $this->store($request, $params)

V kontroleru:

<?php
namespace App\Admin\Controllers;

use function Petrovo\Http\json;

class AdminsController {
    public function index($request, $params) {
        $admins = $this->getAllAdmins();
        return json(['data' => $admins]);
    }

    public function store($request, $params) {
        $data = $request->getParsedBody();
        $admin = $this->createAdmin($data);
        return json(['id' => $admin->id], 201);
    }
}

Výhody array notation:

  • ✅ IDE autocomplete pro metody
  • ✅ Static analysis (PHPStan, psalm)
  • ✅ Explicitní a čitelný
  • ✅ Třída se instancuje jen při matchnutí

4. Middleware: Od Laminas → Petrovo

Staré řešení: Siler\Stratigility (develop)

use Siler\Stratigility\pipe;
use Siler\Stratigility\process;

// Registrace
pipe(proxyMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');

// Aplikace na route
$handler = fn($request, $params) => require_fn(__DIR__ . '/slug.php')($params);
$wrappedHandler = process($request, 'frontend')($handler);

Route\any('/{slug}', $wrappedHandler, $request);

Nové řešení: Petrovo\Middleware (feature/new-routing)

use function Petrovo\Middleware\pipe;
use function Petrovo\Route\{any, dispatch};

// Registrace
pipe(proxyMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');

// Aplikace na route
any('/{slug}', __DIR__ . '/slug.php', 'frontend');  // Middleware se aplikuje automaticky!

// Dispatch
$response = dispatch(request());

Co se zjednoduší:

  • ✅ Middleware se aplikuje automaticky při zaregistrování route s pipeline
  • ✅ Žádný process() wrapper
  • ✅ Žádný require_fn()
  • ✅ Čitelnější kód

Middleware Struktura - Bez Změny

Samotné middleware soubory se nemění:

// app/Middlewares/Auth/authMiddleware.php (stejné v obou verzích)

$authMiddleware = function($request, $handler) {
    // 1. Logika PŘED handlerem
    $token = $request->getHeaderLine('Authorization');

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

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

    // 3. Logika PO handleru
    $response = $response->withHeader('X-Runtime', microtime(true));

    return $response;
};

Middleware jsou PSR-15 kompatibilní v obou verzích.


5. HTTP Odpovědi: Od Siler → Petrovo

Staré: Siler helpers

use Siler\Http\Response;
use Siler\Http\Json;

// Siler helpers
Response\html($content, 200);
Response\redirect($url);
Json\json($data);
HttpHandlerRunner\sapi_emit($response);

Nové: Petrovo helpers

use function Petrovo\Http\{html, json, text, redirect, no_content, emit};

// Petrovo helpers - stejné interface, nižší overhead
html($content, 200);
json($data);
text($content);
redirect($url);
no_content();

emit($response);  // Emit na klient

Změna je přímo kompatibilní - stejná API, jen z jiného namespacu.


6. Konkrétní Příklady Migrace

Příklad 1: Frontend Routes (app/routes/app-frontend.php)

Staré (Siler)
<?php
declare(strict_types=1);

use Siler\Diactoros;
use Siler\Route;
use function Siler\require_fn;
use function Siler\Stratigility\pipe;
use function Siler\Stratigility\process;

$request = Diactoros\request();

// Middleware
pipe(proxyMiddleware, 'frontend');
pipe(redirectionMiddleware, 'frontend');
pipe(defineWebMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');
pipe(logoutMiddleware, 'frontend');
pipe(loginMiddleware, 'frontend');
pipe(csrfMiddleware, 'frontend');
pipe(settingMiddleware, 'frontend');
pipe(headersMiddleware, 'frontend');

// Routes
$newsletterHandler = fn($request, $params) => require_fn(
    __DIR__ . '/../tags/newsletter/add-email/activation.php'
)($params);
Route\get('/newsletter-email-activation', process($request, 'frontend')($newsletterHandler), $request);

$slugHandler = fn($request, $params) => require_fn(
    __DIR__ . '/../Controllers/slug.php'
)($params);
Route\any('/{slug}?', process($request, 'frontend')($slugHandler), $request);
Nové (Petrovo)
<?php
declare(strict_types=1);

use function Petrovo\Http\{html, request};
use function Petrovo\Url\show404;
use function Petrovo\Middleware\pipe;
use function Petrovo\Route\{any, get, dispatch};

// Middleware
pipe(proxyMiddleware, 'frontend');
pipe(defineWebMiddleware, 'frontend');
pipe(sanitizationMiddleware, 'frontend');
pipe(logoutMiddleware, 'frontend');
pipe(loginMiddleware, 'frontend');
pipe(csrfMiddleware, 'frontend');
pipe(settingMiddleware, 'frontend');
pipe(headersMiddleware, 'frontend');

pipe(redirectionMiddleware, 'frontend-redirect');

// Routes
get('/newsletter-email-activation', __DIR__ . '/../tags/newsletter/add-email/activation.php', 'frontend');
any('/', __DIR__ . '/../Controllers/slug.php', 'frontend');
any('/{slug}', __DIR__ . '/../Controllers/slug.php', 'frontend');
get('/.*', __DIR__ . '/../Controllers/slug.php', 'frontend-redirect');

// Dispatch
$response = dispatch(request());

// Fallback 404
if (!$response) {
    $response = html(show404(), 404);
}

return $response;

Příklad 2: Data Endpoint (app/routes/data.php)

Staré (Siler)
<?php
declare(strict_types=1);

use Siler\Diactoros;
use Siler\Http;
use Siler\HttpHandlerRunner;
use Siler\Route;
use function Siler\Stratigility\handle;
use function Siler\Stratigility\pipe;

const DATA_DIR = DIR . CONFIG['dataDir'];
const GET_DATA_URL = '/getData';

$path = Http\path();
if (!str_contains($path, GET_DATA_URL . '/')) {
    return;
}

$request = Diactoros\request();

pipe(proxyMiddleware, 'data');
pipe(defineWebMiddleware, 'data');
pipe(sanitizationMiddleware, 'data');
pipe(corsMiddleware, 'data');
pipe(finalHandler, 'data');

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

$routes = [
    Route\any($path, DATA_DIR . str_replace(GET_DATA_URL, '', $path) . '.php'),
    Diactoros\text($_SERVER['REQUEST_URI'] . " — not found", 404),
];

$response = Route\matching($routes);
HttpHandlerRunner\sapi_emit($response);
exit;
Nové (Petrovo)
<?php
declare(strict_types=1);

use function Petrovo\Http\{request, html};
use function Petrovo\Middleware\pipe;
use function Petrovo\Route\{any, dispatch};

pipe(proxyMiddleware, 'data');
pipe(defineWebMiddleware, 'data');
pipe(sanitizationMiddleware, 'data');
pipe(publicEndpointsMiddleware, 'data');
pipe(corsMiddleware, 'data');

// Dynamic data endpoint routing
any('/getData/.*', function($request) {
    $path = $request->getUri()->getPath();
    $file = DIR . CONFIG['dataDir'] . str_replace('/getData', '', $path) . '.php';

    if (!is_file($file)) {
        return html('Not found', 404);
    }

    return require $file;
}, 'data');

// Match and execute
return dispatch(request());

Příklad 3: CRON Routes (app/routes/cron.php)

Staré (Siler)
<?php
declare(strict_types=1);

use Petrovo\Logger\Logger;
use Siler\Diactoros;
use Siler\Http\Response;
use Siler\Route;
use function Siler\Stratigility\pipe;
use function Siler\Stratigility\process;

$request = Diactoros\request();

pipe(proxyMiddleware, 'cron');
pipe(defineWebMiddleware, 'cron');
pipe(ipFilteringMiddleware, 'cron');

$cronHandler = function ($request, $params) {
    Logger::setLogFile(DIR . '/var/log/cron.log');
    $uriPath = $request->getUri()->getPath();
    $script = __DIR__ . '/../cron/' . $params['script'] . '.php';
    if (is_file($script)) {
        Logger::info('Running cron ' . $uriPath);
        require $script;
    }
    Logger::alert('An attempt was made to start the wrong cron ' . $uriPath);
    Response\output($_SERVER['REQUEST_URI'] . " — not found", 404);
    exit;
};

Route\get(CONFIG['dirPrefix'] . '/CRON/{script}', process($request, 'cron')($cronHandler), $request);
Nové (Petrovo)
<?php
declare(strict_types=1);

use Petrovo\Logger\Logger;
use function Petrovo\Http\{html, request};
use function Petrovo\Middleware\pipe;
use function Petrovo\Route\{get, dispatch};

pipe(proxyMiddleware, 'cron');
pipe(defineWebMiddleware, 'cron');
pipe(ipFilteringMiddleware, 'cron');

// Register cron handler route
get('/CRON/{script}', function ($request, $params) {
    Logger::setLogFile(DIR . '/var/log/cron.log');

    $scriptName = $params['script'] ?? null;
    if (!$scriptName || !preg_match('#^[a-z0-9_-]+$#i', $scriptName)) {
        Logger::alert('Invalid cron script: ' . ($scriptName ?? 'empty'));
        return html('Bad request', 400);
    }

    $scriptPath = __DIR__ . '/../cron/' . $scriptName . '.php';

    if (!is_file($scriptPath)) {
        Logger::alert('Cron script not found: ' . $scriptName);
        return html($scriptName . ' — not found', 404);
    }

    Logger::info('Running cron: ' . $scriptName);
    return require $scriptPath;
}, 'cron');

// Match and execute
return dispatch(request());

7. PSR-7 Přechod: Přechodná Fáze

Co je PSR-7?

PSR-7 (HTTP Message Interface) je standard pro práci s HTTP requesty a responsemi v PHP.

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$request;  // ServerRequestInterface
$response; // ResponseInterface

Staré: Siler\Diactoros (PSR-7, ale přes Siler)

use Siler\Diactoros;

$request = Diactoros\request();  // PSR-7 ServerRequest
Diactoros\html(...);              // PSR-7 Response
Diactoros\text(...);
Diactoros\json(...);

Nové: Petrovo\Http (Native PSR-7)

use function Petrovo\Http\{request, html, json, text, emit};

$request = request();  // PSR-7 ServerRequest (HttpSoft)
html(...);             // PSR-7 Response
json(...);
emit($response);       // Emit na klient

Přechodná Fáze: Mixing

V přechodné fázi bude v kódu mix obou přístupů:

// Nové (Petrovo)
use function Petrovo\Http\{request, html, json};

$request = request();  // Petrovo helper
$data = json(['status' => 'ok']);  // Petrovo helper

// Ale v DATA endpointech a tagech může být ještě Siler:
use Siler\Http\Json;  // Staré
$oldData = Json\json(['status' => 'ok']);

Strategie migrace:

  1. ✅ Již hotovo: app/bootstrap.php, app/routes/*
  2. V přechodné fázi: app/DATA/ endpoints, app/tags/ mohou ještě používat Siler
  3. Postupně: Migrovat DATA a tags na Petrovo helpers
  4. ✅ Cíl: Všechno na Petrovo, Siler zcela odstraněn

Konkrétně v DATA endpointu (přechodně)

<?php
// app/DATA/Users/show.php

declare(strict_types=1);

// STARÉ (ještě funguje)
use Siler\Http\Json;

$id = (int)($_get['id'] ?? 0);
$user = DB::row("SELECT * FROM users WHERE id = ?", [$id]);

// Zatím vrací přes Siler
return Json\json(['status' => 'ok', 'data' => $user]);
<?php
// app/DATA/Users/show.php

declare(strict_types=1);

// NOVÉ (po migraci)
use function Petrovo\Http\json;

$id = (int)($_get['id'] ?? 0);
$user = DB::row("SELECT * FROM users WHERE id = ?", [$id]);

// Vrací přes Petrovo
return json(['status' => 'ok', 'data' => $user]);

8. Checklist Migrace

Kdy má být váš kód migrovaný?

  • Ihned (už hotovo):
  • app/bootstrap.php - path-based dispatcher
  • app/routes/*.php - nový Petrovo\Route API
  • Core middleware - už funguje s Petrovo\Middleware

  • V přechodné fázi (mix je OK):

  • app/DATA/* - mohou být na Siler nebo Petrovo helpers
  • app/tags/* - mohou být na Siler nebo Petrovo helpers
  • app/Controllers/* - mohou být na Siler nebo Petrovo helpers

  • Postupně migrovat:

    // Starý kód (Siler)
    use Siler\Http\Json;
    return Json\json($data);
    
    // Nový kód (Petrovo)
    use function Petrovo\Http\json;
    return json($data);

  • Nikdy:

  • ❌ Nevrácet exit z handleru - vrať Response
  • ❌ Nepoužívat require_fn() - přímé cesty k souborům
  • ❌ Nepoužívat Siler\Stratigility\process() - middleware se aplikuje automaticky
  • ❌ Nepoužívat HttpHandlerRunner\sapi_emit() - použij Petrovo\Http\emit()

9. Typické Chyby a Řešení

Chyba 1: Handler vrací exit místo Response

Špatně:

// Controllers/slug.php (OLD - neprojde)
echo "Hello";
exit;  // ❌ Nevrací Response

Správně:

// Controllers/slug.php (NEW)
use function Petrovo\Http\html;

return html("<h1>Hello</h1>");  // ✅ Vrací Response

Chyba 2: Middleware se neaplikuje

Špatně:

any('/path', __DIR__ . '/../handler.php');  // Bez pipeline!

Správně:

any('/path', __DIR__ . '/../handler.php', 'frontend');  // S pipeline

Chyba 3: Parametry z URL nejsou dostupné

Špatně:

// Controllers/slug.php
$slug = $request->getQueryParams()['slug'];  // ❌ Hledá v query string

Správně:

// Controllers/slug.php
$slug = $params['slug'];  // ✅ Z route parametru
// nebo
$slug;  // ✅ Dostupný přímo (extract z $params)

Chyba 4: Response se emituje manuálně

Špatně:

// app/routes/app-frontend.php
$response = dispatch(request());
emit($response);  // ❌ Emitování je v route souboru
exit;

Správně:

// app/routes/app-frontend.php
$response = dispatch(request());
return $response;  // ✅ Emitování je v bootstrap.php


10. Reference: API Porovnání

Route Registration

Operace Staré (Siler) Nové (Petrovo)
GET route Route\get($path, $handler) get($path, $handler, 'pipeline')
POST route Route\post($path, $handler) post($path, $handler, 'pipeline')
ANY method Route\any($path, $handler) any($path, $handler, 'pipeline')
Dispatch Route\matching($routes) dispatch(request())

Handler Wrapping

Operace Staré (Siler) Nové (Petrovo)
Wrap handler process($request, 'pipeline')($handler) Automatické (žádné wrappování)
Require soubor require_fn($path) Přímá cesta: $path
Parametry Přes $params['name'] Přes $params['name']

Middleware

Operace Staré (Siler) Nové (Petrovo)
Register pipe($middleware, 'pipeline') pipe($middleware, 'pipeline')
Apply process() manuálně Automatické v dispatch()
Execute handle($request, 'pipeline') dispatch(request())

HTTP Responses

Response Staré (Siler) Nové (Petrovo)
HTML Diactoros\html($content) html($content)
JSON Json\json($data) json($data)
Text Diactoros\text($content) text($content)
Redirect Response\redirect($url) redirect($url)
Emit HttpHandlerRunner\sapi_emit() emit($response)

11. Shrnutí

Hlavní Výhody Nového Řešení

  1. Transparentnost - Vidíš přesně, co se děje
  2. Jednoduchost - Méně abstrakčních vrstev
  3. Výkon - Nižší overhead, path-based dispatcher
  4. Čitelnost - Všechny routes v jednom souboru dle typu
  5. Determinismus - Žádné skryté chování

Co Se Zachovalo

  • ✅ PSR-7 kompatibilita (HTTP Message Interface)
  • ✅ PSR-15 middleware (Request Handler Interface)
  • ✅ Parametry route ({slug}, {id}, atd.)
  • ✅ Middleware pipeline (pipe, middlewares)
  • ✅ Pojmenované pipelines (frontend, admin, atd.)

Příští Kroky

  1. ✅ Migrovat app/bootstrap.php - HOTOVO
  2. ✅ Migrovat app/routes/.php - HOTOVO*
  3. Postupně migrovat app/DATA/* na Petrovo\Http
  4. Postupně migrovat app/tags/* na Petrovo\Http
  5. Postupně migrovat app/Controllers/* na Petrovo\Http
  6. Zcela odebrat Siler ze composer.json

Otázky a Podpora

Máš-li dotazy ohledně migrace:

  • Čti dokumentaci: @docs/core/psr/overview.md
  • Čti dokumentaci routování: @docs/app/router.md
  • Čti dokumentaci middleware: @docs/app/middlewares.md
  • Koukni do app/routes/ - jsou příkladem nejnovějšího stylu