File-based PHP router

The filesystem
is the router.

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.

$ composer require cr0w/phorq

Structure
as declaration.

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.

modules/
core/
  middleware.php
  routes/
    index.php
    users/
      index.php
      [id].php
    docs/
      [...rest].php
blogging/
  config.php  ← mount: blog
  routes/
    index.php
    [slug].php
resolves to
/
/users
/users/:id
/docs/*
/blog
/blog/:slug
1file
To add a route
0ms
Lookup — cached
0dep
Runtime dependencies
~600loc
Entire router
Route file
Bootstrap
modules/core/routes/users/[id].php
// $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)),
);
public/index.php
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;

One callable.
Three places.

global
router->use() — wraps everything
core
core/middleware.php — every request
module
account/middleware.php — module only
handler
the route file
public/index.php — global
$router
    ->use(new SessionMiddleware())
    ->use(new CspMiddleware(['default-src' => "'self'"]));
modules/account/middleware.php — module
return function (callable $next, Request $req, AppContext $ctx): mixed {
    if (!$ctx->auth) {
        return $req->isHtmx()
            ? htmxRedirect('/login')
            : redirect('/login');
    }
    return $next();
};