dianaphp / router
Attribute-based HTTP and console routing for Diana PHP
Requires
- php: ^8.5.12
- dianaphp/lib: dev-main
- dianaphp/sdk: dev-main
Requires (Dev)
- dianaphp/framework: dev-main
- phpunit/phpunit: ^11.5
Suggests
- dianaphp/framework: Required for Framework\RouterModule and the framework command controllers.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-07 00:26:12 UTC
README
dianaphp/router provides attribute-based HTTP and console routing. The package
requires PHP 8.5.12+, dianaphp/sdk and dianaphp/lib. Router and framework can each
be installed independently.
Package structure
src/Diana/Router/
FileRouter.php, FileRouterCached.php, Route.php, ... standalone routing
Attributes/, Exceptions/ standalone metadata
Framework/
RouterModule.php framework integration
Controllers/ framework commands
dianaphp/framework is a Composer suggestion for applications using the module,
and a development dependency for integration tests. Consumers installing the
router as a dependency do not pull in the framework.
The SDK supplies container, router, route and cache contracts and request
interfaces. Diana\Contracts\IO\HttpRequest exposes getRoute() and
getMethod(); ConsoleRequest exposes getCommand() and getArguments().
Both extend RequestContract. Custom requests can implement these interfaces.
Standalone usage
Bind Diana\Contracts\Router\Route to Diana\Router\Route in your container.
Pass your implementation of the SDK container contract to FileRouter, then
register an arbitrary object containing route attributes:
use Diana\Router\Attributes\Command; use Diana\Router\Attributes\Get; use Diana\Router\FileRouter; class ExampleController { #[Get('/hello/:name')] public function hello(string $name): string { return 'Hello ' . $name; } #[Command('hello')] public function command(): string { return 'Hello CLI'; } } $router = new FileRouter($container); $router->generateRoutesFromPackage(new ExampleController()); $route = $router->resolve($request);
resolve() returns route metadata; the caller invokes the controller and runs its
middleware. No framework lifecycle or event dispatcher is needed. For caching,
construct FileRouterCached($container, $cache) with an SDK cache implementation.
It restores cached metadata on construction. When isLoadedFromCache() is false,
register the controller objects and call generateCache() explicitly.
HTTP and command semantics
HTTP methods are case-sensitive uppercase names. Leading and trailing slashes are ignored; parameter segments must be nonempty. Matching is per HTTP method: the route with the most literal segments wins, regardless of discovery order. Ties retain registration order. This also applies to cached routes.
- Unknown paths throw
RouteNotFoundException(framework response: 404). - A known path with an unsupported method throws
MethodNotAllowedException.getAllowedMethods()returns a unique, alphabetically sorted list. The module responds with 405 and anAllowheader; it does not invoke the 404 handler. #[Head('/path')]takes precedence over GET fallback. Otherwise HEAD uses the matching GET route, including its middleware and controller. The application removes the response body after global middleware, retaining status and headers, including on errors. Standalone callers must suppress HEAD bodies themselves.#[Options('/path')]takes precedence over automatic OPTIONS. For other known paths,resolve()throwsOptionsRequestExceptionwithgetAllowedMethods(); the module returns an empty 204 response withAllow, without route middleware or controller execution. Unknown paths still produce 404.Allowincludes all methods matching the path, HEAD when GET exists, and OPTIONS. Explicit OPTIONS handlers own their response and headers.
Duplicate HTTP method/path pairs and duplicate command names throw
DuplicateRouteException; the original definition remains registered. Command
names are case-sensitive and global across controller packages. Positional
arguments map to the declared names; omitted values allow controller defaults.
Existing CLI option syntax remains --name value or --flag (a following token
that does not start with -- is consumed as its value).
For framework calls prefer an explicit list, preserving spaces and empty values:
$app->runCommand('hello', ['two words', '', '--format', 'json']);
The one-string form remains supported: whitespace separates tokens outside
single/double quotes; quotes are removed and may concatenate token fragments.
Empty quotes produce an empty argument. Backslash escapes the next character
outside single quotes, including inside double quotes. There is no variable,
glob, command or shell expansion. Empty command names, unclosed quotes and
trailing escapes throw InvalidArgumentException before dispatch. With an
explicit second argument, the command name and arguments are passed verbatim.
Framework module
Diana\Router\Framework\RouterModule connects the package to the framework.
Its constructor supplies default Router → FileRouter and Route → Route
bindings when the application has not supplied its own. It subscribes to
RegisterPackageEvent to discover controllers; for a cold FileRouterCached,
it also subscribes to BootEvent to write the cache. A warm cache skips discovery.
These lifecycle subscriptions live entirely in the module.
RouterModule::__invoke(RequestContract $request): Response owns framework
routing and controller execution. Its routing pipeline resolves a route or an
error route, registers the resolved route in the container and delegates to a
nested pipeline containing route middleware and controller execution.
Route parameters and statusCode are injected into the controller method.
Returning from route middleware without $next() skips the controller.
It also skips construction and execution of later route middleware. Use
return $next() to keep the request or return $next($request) to pass a
replacement to later middleware and controller injection. The resolved route
stays the same. Middleware that called $next can process the returned response,
including a response from a later middleware abort.
HTTP 404 and console status 1 fallbacks remain available. A controller's returned
Response is preserved; other results are wrapped with the resolved status.
The application's module registers the router module before its controllers and delegates request processing to it:
use Diana\Contracts\RequestContract; use Diana\Framework\Core\Application; use Diana\IO\Response; use Diana\Router\Framework\RouterModule; class AppModule { private RouterModule $router; public function __construct(Application $app) { $this->router = $app->registerPackage(RouterModule::class); $app->registerPackage(AppController::class); } public function __invoke(RequestContract $request): Response { return ($this->router)($request); } }
Framework global middleware wraps this entire delegation, including requests
without a matching route, and can stop processing before resolution.
Modules and controllers are ordinary classes registered through the same API.
A module may itself contain attributed controller methods. Only a class used
as the configured application entry point needs __invoke().
Alternatively, configure the router module directly and list the controller packages explicitly:
return [ 'entrypoint' => \Diana\Router\Framework\RouterModule::class, 'packages' => [ \Diana\Router\Framework\RouterModule::class, \App\AppController::class, \Diana\Rendering\RenderingPackage::class, ], ];
To select FileRouterCached, configure the router binding in singleton and
bind its cache dependency to PhpFileCache through contextualBindings.
Register the module before controllers so that discovery is ready during boot.
The existing framework command controllers retain their Diana\Controllers
namespaces; their files live in Framework/Controllers/.
Local development
The workspace path repository includes this package as dev-main.
Use matching local SDK and framework sources for integration tests.
From the workspace root:
composer install composer test:router php vendor/bin/phpunit router/tests/Framework
For a separate checkout with the local Diana path repository configured:
composer install composer test composer test -- --testsuite router composer lint
The package's router PHPUnit suite covers standalone routing and caching;
framework-integration covers the optional module. Development installation
includes the framework for these integration tests. The standalone suite is also
verified from a consuming Composer project without the framework installed.