A component that turns your app into a web app: attribute-based routing, HTTP requests and responses, middleware, sessions, and security.
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.
serve) and generatorsYou decide which routes exist, what middleware guards them, and how each request is answered — the component supplies the transport, matching, and plumbing.
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.
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.
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 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.
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.
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.
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). |
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.
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.
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.
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.