Router
Triangle Router matches an HTTP method and path to a callback, extracts parameters, stores route middleware and generates named URLs. Engine-HTTP invokes the controller and creates the response; dispatch itself only reports a match.
Installation and registration
Section titled “Installation and registration”composer require triangle/routerRequirements: PHP ^8.1 and triangle/engine ^8.1. The entry point is Triangle\Router.
use Triangle\Router;
Router::get('/users/{id:\d+}', static function ($request, string $id) { return ['id' => $id];})->name('users.show');Engine-HTTP passes request first. Captured parameters remain strings, including numeric matches. Resource existence, authorization and value ranges are application checks.
Get/post/put/patch/delete/head/options register one method; any registers *. Route accepts a method string or array. Callbacks can be closures, [Controller::class, 'method'] or Controller@method. Registration checks class/method existence; HTTP Engine resolves instances and dependencies.
Patterns and groups
Section titled “Patterns and groups”A default {id} matches a nonempty segment without /; {id:\d+} adds a constraint. Optional segments such as /articles[/{slug}] must occur at the end. Invalid bracket structure is rejected.
Router::group('/api', static function (): void { Router::get('/health', static fn ($request) => ['ok' => true]);});Groups accumulate prefixes and run their callback immediately during registration, not during request handling.
Building the dispatcher
Section titled “Building the dispatcher”Triangle bootstrap loads application and enabled-plugin route.php files, then builds dispatch data. For standalone use, call Router::collect([]) after registration before calling dispatch.
An empty path list rebuilds from existing registrations; it does not clear them. Late registrations do not appear in the existing dispatcher until another collect. Files are loaded with require_once, so collect is not a hot reload mechanism. Restart application workers for changed route files. This implementation has no public Router::clear.
Dispatch results
Section titled “Dispatch results”FOUND contains callback, named parameter strings and RouteObject. METHOD_NOT_ALLOWED contains allowed methods. NOT_FOUND has no matching path. Inspect the first element before accessing the remaining values.
HEAD falls back to GET when no HEAD route matches, then * routes are considered. Removing the response body for HEAD and emitting Allow headers belong to the HTTP layer.
Named links
Section titled “Named links”$url = Router::getByName('users.show')?->url(['id' => 42, 'page' => 2]);This yields /users/42?page=2. Remaining parameters become a query string; an unknown name returns null. Values are not validated against route constraints or encoded as path segments. Validate and, when required, rawurlencode individual segments before substitution.
Url with no parameters returns the raw pattern, including placeholders and optional brackets. Missing parameters can leave unresolved placeholders; the result is not automatically a usable link.
Middleware and fallback
Section titled “Middleware and fallback”Route middleware accepts a class or list. Arrays are reversed on storage and processed again by Middleware Bootstrap; verify ordering when mixing registration forms.
Fallback registers an HTTP callback but does not change a direct dispatch NOT_FOUND result. GetFallback builds it with withGlobal=false, so global middleware policy does not automatically apply to this callback.
Router API
Section titled “Router API”Get/post/put/patch/delete/head/options(path, callback) register one method; any uses *; route(methods, path, callback) accepts an explicit method list. Group(prefix, callback) returns the group Router. Collect(paths) loads configuration and builds dispatch data; dispatch does not invoke the handler.
Resource(name, controller, actions = null, prefix = ”) registers existing index/store/create/edit/show/update/patch/destroy methods, or an explicit action list. Standard paths follow /name and /name/{id}, with /create and /{id}/edit. Additional explicit actions use any /name/action[/{id}]. The patch route name omits prefix in this implementation, unlike the other standard actions.
Child(path, callback = null) does not apply path and invokes callback without a null check. Use group for prefix grouping.
RouteObject
Section titled “RouteObject”GetName/name, getPath, getMethods, getCallback, middleware/getMiddleware, param/setParams and url expose route metadata. Naming a second route with the same name replaces its registry entry. SetParams merges values; param without a name returns the map.
Url substitutes named values or numeric positional values, then builds a query with http_build_query. It does not enforce constraints, completeness or path escaping.
Dispatch shape
Section titled “Dispatch shape”- FOUND:
[Dispatcher::FOUND, callback, parameters, routeObject]. - METHOD_NOT_ALLOWED:
[Dispatcher::METHOD_NOT_ALLOWED, allowedMethods]. - NOT_FOUND:
[Dispatcher::NOT_FOUND].
Parameters are named strings. Dispatch expects the path prepared by the HTTP layer, not an arbitrary full URL with a query string.
Default routes and fallback
Section titled “Default routes and fallback”DisableDefaultRoute(plugin = ”, app = null) disables default routing by scope. Its implementation contains a controller/action-array branch, but the argument type is ?string: PHP rejects arrays before reaching it. IsDefaultRouteDisabled also checks controller/action attributes.
Fallback(callback, plugin = ”) returns a RouteObject. GetFallback(plugin, status = 404) caches the assembled HTTP callback by plugin, not by status. Repeated status values for the same plugin do not create independent chains.
Instance, routeParser, dataGenerator, data and routes expose the current registry. Defaults are Std parsing and GroupCountBased generation/dispatch. They do not provide a public reset or a concurrent route-table update protocol.