KoniKodeKnowledge BaseMy AccountOpen website

KNOWLEDGE BASE

Documentation

DOCUMENTATION
CURRENT APPLICATIONClean Framework
DocumentationClean Framework

Clean Framework · DOCUMENTATION

Routing and middleware

Routing and middleware

Bounded candidate reference — 2026-10-20: 0.2.0-rc.1 is committed locally through 84f415b3098af5653577b2c65eea31fca21df79c, unpublished and not production-approved; stable PHP >=8.5.11 <8.6.0 is required. This is documentation, not adoption: this knowledge base retains its installed 0.1.0-rc.1 dependency and PHP 8.2.11 local profile. Dated PHP 8.2 evidence is historical; earlier dirty-source reports retain their original test provenance, not retroactive clean-commit proof. Candidate examples are illustrative: API signatures were reviewed, but candidate code was not executed by this editorial update. See current checkpoint, open criteria and adoption boundary.

Direct ModuleMiddleware objects participate in RouteVisibility before method negotiation. OFF canonical routes conceal OPTIONS/405/Allow; mandatory security remains independent. See module policy.

Router is an application-owned instance. Route declarations are mutable only during composition; the first dispatch or named URL generation validates and freezes every definition. Later registration or mutation fails so request-time code cannot change a reviewed security pipeline.

Routes remain declaration ordered. Declare literals before broader parameter and catch-all patterns. Duplicate method/pattern pairs and duplicate route names fail deterministically.

Methods and patterns

The router supports GET, POST, PUT, PATCH, DELETE, explicit HEAD, explicit OPTIONS, and multi-method declarations. GET provides a HEAD fallback unless an explicit HEAD definition matches. Automatic OPTIONS returns the full Allow set of visible definitions; a known visible path with the wrong method raises 405 with the same header. Direct middleware objects implementing RouteVisibility are consulted after path/constraint matching, before any method selection or Allow accumulation. All must permit; hidden-only paths return404 without Allow. Hidden explicit HEAD/OPTIONS cannot mask visible fallbacks. Unrestricted/enabled definitions retain the same method semantics.

$router->get('/reports/current', CurrentReportController::class . '::show');
$router->get('/reports/{id}', ReportController::class . '::show')
    ->name('reports.show')
    ->where('id', 'int');
$router->get('/archive/{year?}/{month?}', ArchiveController::class)
    ->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]']);
$router->get('/media/{path*}', MediaController::class . '::show');

{id} captures one decoded segment, terminal {name?} segments are optional, and terminal {path*} captures decoded segments while retaining slashes. Optional segments must remain at the end, and catch-all parameters must be the last segment. Encoded slashes are rejected in ordinary single-segment values.

where() accepts a bounded regular-expression fragment or int, alpha, alnum, slug, or uuid. Constraints apply equally to matching and URL generation. A constraint for a missing parameter or an invalid expression fails during atomic router finalization.

Visibility is trusted request-independent availability metadata, not user authorization or speculative middleware execution. ModuleMiddleware supplies this contract using the same policy object as dispatch; its published fluent object usage needs no migration. Unresolved service IDs and callable wrappers are not instantiated/inferred during discovery. Use an explicit object and keep mandatory security middleware. See module adoption.

Names, URL generation, and base paths

Route names use lowercase dotted identifiers. URL generation encodes each ordinary segment, encodes catch-all segments individually, enforces route constraints, rejects missing required or unexpected parameters, omits missing optional segments, and prepends the router's configured application base path.

$router = new Router($container, '/control-panel');
$router->get('/users/{id}', UserController::class . '::show')
    ->name('users.show')
    ->where('id', 'int');

$router->url('users.show', ['id' => 42]); // /control-panel/users/42

Application composition must give Router the same http.base_path used to construct Request. A path is generated, not accepted as a redirect target; query strings remain an explicit caller concern.

Groups, defaults, and metadata

The historical prefix-only group form remains valid. The structured form adds name prefix, inherited middleware, defaults, constraints, and namespaced route metadata. Group middleware runs before route middleware and cannot remove global kernel middleware.

$router->group([
    'prefix' => '/admin',
    'name_prefix' => 'admin',
    'middleware' => [$authenticate, $authorize],
    'defaults' => ['locale' => 'en'],
    'constraints' => ['id' => 'int'],
    'metadata' => ['app.area' => 'administration'],
], static function (RouteGroup $routes): void {
    $routes->get('/users/{id}/{tab?}', UserController::class . '::show')
        ->name('users.show')
        ->where('tab', 'slug')
        ->metadata(['app.page_kind' => 'detail']);
});

Matched values are exposed through immutable RouteContext at RequestAttribute::ROUTE_CONTEXT. It carries name, pattern, actual parameters, defaults, methods, and namespaced framework.*/app.* metadata. The handler's second argument remains the merged default/parameter array for documented backward compatibility.

Canonical paths

CanonicalPathPolicy makes duplicate-slash and non-root trailing-slash behavior explicit:

  • normalize retains the historical internal normalization without a redirect;
  • redirect_safe issues a same-origin 308 only for GET/HEAD and rejects aliases for unsafe methods rather than risking a replay;
  • strict rejects every non-canonical alias.

The skeleton selects the mode through http.canonical_path_mode or HTTP_CANONICAL_PATH_MODE. A safe redirect preserves the configured base path and query string. It never changes POST, PUT, PATCH, or DELETE into another request.

REDIRECT_SAFE keeps its route-independent308 for all safe aliases, including unknown or hidden paths; the canonical target then enforces visibility. Making only hidden aliases return404 would itself disclose existence. Strict and unsafe-alias rejection do not change. No controller or arbitrary middleware factory is resolved for normalization or visibility.

Handlers

A handler may be a closure, [object-or-class, method], ClassName::method, or an invokable controller class. It may accept zero arguments, a Request, or a Request plus the merged route arguments. It must be public and require at most two arguments. A Response is returned directly, an array becomes a success JSON envelope, and other values become HTML text.

Controller class strings are resolved only at the innermost handler boundary, after route security middleware has granted access.

Middleware contract and order

New middleware implements the framework's own typed contract:

final class ExampleMiddleware implements Middleware
{
    public function process(Request $request, RequestHandler $next): Response
    {
        // Validate or enrich the immutable request before controller creation.
        return $next->handle($request);
    }
}

The shape is inspired by PSR-15 but does not claim PSR compliance because the framework uses its own request and response values. Middleware is entered left to right and unwound right to left. Application composition may inject middleware objects or container service IDs. Documented callable middleware is temporarily supported through an invokable CallableRequestHandler; new code should use RequestHandler::handle().

Global kernel middleware always wraps route/group middleware. Applications must place mandatory correlation, canonical host/scheme, exception, and other global protections there. Route groups can add authentication, authorization, recent-authentication, content-type, CSRF, rate-limit, anti-bot, administrative credential, webhook signature/idempotency, or application middleware; they do not silently exempt a route from global policy.

The neutral skeleton orders SecurityHeadersMiddleware outside CorrelationIdMiddleware, which is outside ExceptionMiddleware. That order allows translated error responses to unwind through correlation and baseline browser headers. The kernel exception handler remains a fallback for failures in outer middleware or the translator itself.

BodyLimitMiddleware applies a route ceiling below the global request limit. ContentTypeMiddleware declares accepted request media, and ContentNegotiationMiddleware returns 406 unless the client accepts one of a route's response media types; successful negotiated responses carry Vary: Accept. SecurityHeadersMiddleware adds nosniff, frame, referrer, and permissions defaults without replacing explicit response values. HSTS is deployment-owned and is not enabled by generic middleware.

Webhook signature format and route composition remain application/provider policy. A webhook route must pass exact raw bytes to the Phase 6 WebhookReceiver with its provider-specific verifier instead of treating “public” or “CSRF exempt” as sufficient. The earlier Phase 2 isolated test retains its application-defined HMAC fake; reusable receipt, timestamp, deduplication, private-payload, queue, and replay mechanics are documented in PROVIDERS_AND_WEBHOOKS.md.

Error negotiation

A request expects JSON when it explicitly accepts a JSON media type, carries a JSON content type, or uses the conventional XMLHttpRequest header. Wildcard Accept: */* remains HTML-neutral. The exception handler uses the same safe message in HTML or JSON and retains status headers such as Allow, Retry-After, or WWW-Authenticate.

Editorial provenance

Source: docs/ROUTING_AND_MIDDLEWARE.md. Source SHA-256: 02257c3670a1df60c8725a62b471a7784b395bdb1b8050c855a6660b01400b56.

Framework candidate reference; editorial checkpoint 2026-10-20. See candidate status and adoption boundary.

EXPLORE FURTHER

Drag a divider or use Left and Right Arrow keys to resize. Hold Shift for larger steps. Enter hides or restores the sidebar; Home and End select its minimum or maximum width. Double-click resets the width.