ForgeRouter

A component that turns your app into a web app: attribute-based routing, HTTP requests and responses, middleware, sessions, and security.

Overview

ForgeRouter is what makes the Kernel speak HTTP. It's the component that receives a request, matches it to one of your app's controller methods, runs the middleware you've chosen, and sends back a response. If you started from an HTTP blueprint or a web starter, it's already wired in; otherwise you add it like any capability.

What it gives you

  • Attribute-based routes on your controllers
  • URL parameters with constraints and casting
  • A full HTTP Request and Response model
  • Global, web, and API middleware groups
  • Sessions and CSRF protection built in
  • CORS, CSP, rate limiting, circuit breaker
  • Route + hook caching for speed
  • A dev server (serve) and generators

You decide which routes exist, what middleware guards them, and how each request is answered — the component supplies the transport, matching, and plumbing.

Routes & Controllers

Routes are declared as PHP attributes on controller methods. The router discovers your controllers automatically — from your app's controller directory and from every module's — so there are no route registries to maintain.

use Modules\ForgeRouter\Routing\Endpoint;
use Modules\ForgeRouter\Routing\Routable;
use Modules\ForgeRouter\Http\Response;

#[Routable(prefix: '/blog')]
final class BlogController
{
    #[Endpoint('/posts/{id}', 'GET')]
    public function show(int $id, Request $request): Response
    {
        return $this->view('blog/show', ['id' => $id]);
    }
}

Key attribute options:

Attribute Where What it does
#[Endpoint] method Declares a route. Repeatable. Takes path, method, middleware, permissions, override.
#[Routable(prefix)] class Prefixes every endpoint path in that controller.
#[ApiRoute] method Shortcut that places the route under /api/v1.
#[UseMiddleware] class/method Adds middleware (repeatable) to the route.
#[Layout] class/method Chooses the view layout for the response (combined with ForgeView).
#[RequiresRole] class/method Attaches a required role to the route.

Route parameters use braces with optional constraints. The router matches the constraining pattern and casts the value to the controller parameter's declared PHP type (int, float, or bool). A parameter named request receives the current Request object.

Pattern Matches
/posts/{id} letters, digits, underscore, dash
/posts/{id:\d+} the custom regex \d+
/files/{path:.+} any path, greedy (can include slashes)
/x/{slug:no-slash} a single segment with no slash

For API-first controllers, #[ApiRoute] nests routes under /api/v1 automatically, brings the api middleware, and pairs with the ApiResponse helpers for JSON.

Requests

The router builds a Request object from the incoming request. It's available as the container's Request instance, via the request() helper, or as a request parameter on any controller method.

$title    = $request->input('title');      // POST first, then query
$page     = $request->query('page', 1);     // query string only
$dump     = $request->all();                // GET + POST merged
$token    = $request->json('token');        // JSON body (cached)
$auth     = $request->getHeader('Authorization');
$clientIp = $request->getClientIp();        // proxy-aware, spoof-safe
$file     = $request->file('avatar');       // UploadedFile or null
$isTls    = $request->isSecure();           // https-aware

Method spoofing is built in: a _method field or JSON body lets forms issue PUT, PATCH, and DELETE. Cookies surface as Cookie objects via cookies() and cookie(). Middleware can tag the request with setAttribute()/getAttribute(), which is how the router exposes the matched route (_route) and required permissions/roles.

Uploaded files become UploadedFile objects with safe moveTo() (verify-then-rename), getClientFilename(), and stream access — no direct $_FILES juggling in your app.

Responses

Controllers build and return a Response. Anything a controller returns is wrapped in one, but you'll usually construct them directly for full control over status, headers, and cookies.

use Modules\ForgeRouter\Http\Response;
use Modules\ForgeRouter\Http\Cookie;

$response = (new Response('<h1>Hello</h1>', 200))
    ->setHeader('Content-Type', 'text/html')
    ->setCookie(new Cookie('theme', 'dark', 86400));

There are several ready-made helpers:

Helper Purpose
Redirect::to($uri, $status) Redirect response (302 by default)
Redirect::back($request) Redirect to the referer
new ApiResponse($data, $status) JSON envelope with data and optional meta
ResponseHelper::jsonResponse() JSON with Content-Type header
ResponseHelper::csvResponse() CSV download attachment
ResponseHelper::downloadResponse() Download a string as a file

The ResponseHelper trait also includes JSON-aware createResponse() and createErrorResponse() that hand API clients JSON and others HTML.

Middleware

Middleware wraps the request/response lifecycle. A middleware receives the request and calls next() to continue; it can short-circuit with a response. You compose them in groups, and the router runs the global group plus the route's own middleware.

use Modules\ForgeRouter\Http\Middleware;
use Modules\ForgeRouter\Http\Request;
use Modules\ForgeRouter\Http\Response;

final class MaintenanceMiddleware extends Middleware
{
    public function handle(Request $request, callable $next): Response
    {
        if (app_is_down()) {
            return new Response('Be back soon.', 503);
        }
        return $next($request);
    }
}

Middleware is assigned two ways: group definitions in config/middleware.php and per-route #[UseMiddleware]. Groups can also be registered by modules at boot. The engine itself wires up essential middleware automatically:

Middleware (built in) Group What it does
Observability global Tracks request spans and status for observability.
RateLimit global Per-IP sliding-window rate limiting; 429 with headers.
CircuitBreaker global Trips after repeated 5xx; serves 503 to protect the app.
SanitizeInput global Escapes GET/POST/REQUEST input before your controller sees it.
Session web Starts and saves the session.
Csrf web Validates CSRF tokens on state-changing requests; 419 on failure.

More built-ins you can turn on per group or route: CORS, Compression, HttpCache (ETag/Last-Modified), IpWhiteList, RelaxSecurityHeaders (CSP), Cookie, api (ApiMiddleware), and ApiKey.

The generate:middleware command scaffolds a new middleware class for your app or a module. Use #[UseMiddleware('name')] to attach it to a route or controller.

Sessions & CSRF

The web middleware group starts a session and protects your forms against cross-site request forgery. Helpers make this painless in templates:

// In a view or layout
<?= csrf_input() ?>       // hidden _token field
<?= csrf_meta() ?>        // meta tag for JS clients
<?= window_csrf_token() ?> // window.csrfToken for modern JS

The middleware validates the token on every POST/PUT/PATCH/DELETE (via _token or the X-CSRF-TOKEN header) and returns a 419 on a mismatch. Tokens are signed by TokenManager using your app key and expire after a day.

// Session access via the Session class
$session->set('user_id', 42);
$id = $session->get('user_id');
$session->setFlash('success', 'Saved!');
$session->regenerate();

Flash messages are available to your next request and auto-clear after it's read — handy for echoing "saved" feedback after a redirect.

Security & Hardening

ForgeRouter bundles a set of hardening features you turn on with configuration. They're opt-in so you only enable what your app needs.

Feature Config key Behavior
CORS forge_router.cors Whitelist origins/methods/headers; 403 on mismatch, echo CORS headers on match.
CSP forge_router.csp Build a Content-Security-Policy header, merged with external asset sources.
IP whitelist forge_router.ip_whitelist Reject clients not in the list with 403; no-op when empty.
Input sanitizing engine (global) Recursively escapes all incoming GET/POST/REQUEST.
Rate limiting forge_router.rate_limit DB-backed per-IP window limits, 429 + Retry-After headers.
Circuit breaker forge_router.circuit_breaker Stops hammering a failing app; serves 503 after repeated 5xx.
API keys ApiKeyMiddleware Validate X-API-KEY against the api_keys table and required permissions.

Client IP detection prefers REMOTE_ADDR and only falls back to forwarded headers when it's absent, so a spoofed X-Forwarded-For can't bypass rate limiting or whitelists.

Hooks & Extension Points

Beyond middleware, you can hook into the request lifecycle from module code with an attribute on a public method:

use Modules\ForgeRouter\Events\RouterHookAttribute;
use Modules\ForgeRouter\Events\RouterHookName;

#[RouterHookAttribute(RouterHookName::BEFORE_REQUEST)]
public function onBefore(Request $request): void
{
    // inspect or alter the request before routing
}

The three hooks — BEFORE_REQUEST, AFTER_REQUEST, and AFTER_RESPONSE — fire around the kernel, and their compiled callbacks are cached for speed.

The router also recognizes a set of contracts that let capabilities plug in cleanly:

Contract Extends
RequestPreprocessorInterface Modify a request before routing.
ResponseTransformerInterface Modify a response before it's sent.
RouteModifierInterface Mutate route data at registration.
RouteScopeFilterInterface Filter which routes register per scope (e.g. tenant vs central).
ErrorHandlerInterface Final handler for an uncaught exception.
ExceptionHandlerInterface Chainable interceptor; return a Response or pass through.
RequestCollectorInterface Collect request/response data (used by debug tools).

Errors & Maintenance

Unmatched URLs return a rendered 404 page, and uncaught exceptions flow to your error handling (via the ErrorHandlerInterface). The ErrorPageRenderer looks for a custom {code}.php template in your app or modules before falling back to its default page.

# Put your app into maintenance mode (serves 503)
php forge.php down

# Bring it back
php forge.php up

Maintenance writes a 503 page under storage/framework/. The circuit breaker reuses the same renderer to protect a failing app.

The Command Line

ForgeRouter ships the dev server and a few generator and maintenance commands. Five are core commands available directly; two lifecycle commands are scoped to the module.

Command Purpose
serve Start the PHP dev server. Options: --host (localhost), --port (8000).
generate:endpoint Scaffold a controller/endpoint (app or module).
generate:middleware Scaffold a middleware (app or module).
down / up Enable / disable maintenance mode.
modules:forge-router:init Scaffold public entry point and middleware/config files.
modules:forge-router:cleanup Remove the scaffolded files (used on uninstall).

In day-to-day development you'll mostly reach for serve. It compiles the hook cache, then runs the built-in PHP server against your public/ directory.

Configuration

Everything lives under the forge_router config key, with sensible defaults. You can override each via your environment file. The defaults:

# CORS
CORS_ALLOWED_ORIGINS=*
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization

# Rate limiting (enabled, 40 req / 60s; off in dev; localhost bypassed)
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MAX_REQUESTS=40
RATE_LIMIT_TIME_WINDOW=60

# Circuit breaker (5 failures / 300s)
CIRCUIT_BREAKER_MAX_FAILURES=5
CIRCUIT_BREAKER_RESET_TIME=300

# CSP header, off by default
CSP_ENABLED=false

# IP allow-list, empty = allow all
IP_WHITE_LIST=

# Sessions / CSRF
SESSION_ENABLED=true
APP_KEY=your-secure-app-key

The default middleware groups live in config/middleware.php: global, web, and api. Engine middlewares are merged into these automatically.

Installation

ForgeRouter is a core capability, so a web starter or an HTTP blueprint typically includes it already. If your project doesn't have it, add it like any capability:

# Install with the interactive wizard
php forge.php package:install-module

# Or name it directly
php forge.php package:install-module --module=ForgeRouter

Install runs modules:forge-router:init to scaffold the public entry point and config; uninstall runs the cleanup command. It needs no database of its own unless you enable the DB-backed rate limiter or circuit breaker (their tables ship as migrations).

Composing your app: ForgeRouter is the transport almost everything else runs on. Pair it with ForgeView to render page responses, ForgeAuth for authentication middleware, or ForgeWire for reactive pages. See all capabilities to keep building.