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:
- ✅ Již hotovo: app/bootstrap.php, app/routes/*
- V přechodné fázi: app/DATA/ endpoints, app/tags/ mohou ještě používat Siler
- Postupně: Migrovat DATA a tags na Petrovo helpers
- ✅ 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 dispatcherapp/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 helpersapp/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
exitz 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žijPetrovo\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í
- Transparentnost - Vidíš přesně, co se děje
- Jednoduchost - Méně abstrakčních vrstev
- Výkon - Nižší overhead, path-based dispatcher
- Čitelnost - Všechny routes v jednom souboru dle typu
- 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
- ✅ Migrovat app/bootstrap.php - HOTOVO
- ✅ Migrovat app/routes/.php - HOTOVO*
- Postupně migrovat app/DATA/* na Petrovo\Http
- Postupně migrovat app/tags/* na Petrovo\Http
- Postupně migrovat app/Controllers/* na Petrovo\Http
- 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