Drop a file in modules/*/routes/ and it is a route. No registration. No config. No controllers. Dynamic segments, catch-alls, middleware — all expressed as directory structure.
The directory tree is the route map. phorq scans your modules folder once at boot and caches the result as a plain PHP array. On subsequent requests it is a single require.
Dynamic segments are folder or file names: [id] captures a segment, [...rest] catches everything beyond, [[...opt]] makes it optional. Ambiguity is detected at scan time, not at runtime.
Modules mount at named paths. The core module is the fallback. Each module carries its own middleware and routes without touching the others.
// $id, $req, $ctx available in scope $user = user\query::find($id) ?: notFound(); if ($req->isPost()) { user\cmd::update($id, $req->input); return redirect("/users/{$id}"); } return h('.card', h('.eyebrow', $user['name']), h('.user-form', userForm($user)), );
use phorq\Router; $router = Router::create(__DIR__ . '/../modules'); $result = $router->route($ctx); if (!$result) { http_response_code(404); echo '404 Not Found'; return; } echo $result->value;
$router ->use(new SessionMiddleware()) ->use(new CspMiddleware(['default-src' => "'self'"]));
return function (callable $next, Request $req, AppContext $ctx): mixed { if (!$ctx->auth) { return $req->isHtmx() ? htmxRedirect('/login') : redirect('/login'); } return $next(); };