lsr / routing
Laser framework core - Routing.
Requires
- php: >= 8.4
- laravel/serializable-closure: ^2.0
- lsr/helpers: ^0.3.0
- lsr/interfaces: ^0.3.20
- lsr/request: ^0.3.0
- nette/di: ^3.2
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2
- phpunit/phpunit: ^12
- roave/security-advisories: dev-latest
- symfony/console: ^7.3 || ^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 14:56:56 UTC
README
lsr/routing provides HTTP route registration and matching for the Laser framework, with named routes, route groups, middleware, controller attributes and compiled route caches. Its namespace is Lsr\Core\Routing.
Requirements
- PHP
>= 8.4. - LSR interfaces
^0.3.20, helpers^0.3.0and request^0.3.0. - Nette DI
^3.2, Laravel Serializable Closure^2.0, and PSR HTTP server middleware/handler interfaces^1.0. - No PHP extensions are declared directly; dependencies may impose additional platform requirements. See composer.json.
- A writable cache location when compiled caching is enabled. The optional route-cache console commands need Symfony Console, which is a development dependency rather than a runtime requirement of this package.
Installation
composer require lsr/routing
Registering and matching routes
Registration and matching can be used without constructing the full LSR application:
require __DIR__ . '/vendor/autoload.php'; use Lsr\Core\Routing\Router; use Lsr\Enums\RequestMethod; use Nyholm\Psr7\Response; $router = new Router(); $router->get('/health', static fn() => new Response(200, [], 'OK')) ->name('health'); $route = Router::getRoute(RequestMethod::GET, ['health']); echo $route?->getName(); // health
This example matches a route; it does not execute its handler or emit a response. HTTP dispatch is an application responsibility (provided by lsr/core in the framework). See Router and Route for the fluent APIs.
Paths support required parameters such as /users/{id} and optional parameters such as /users/[id]. getRoute() accepts path segments and a by-reference parameter array. Named routes are available through getRouteByName().
Route tables are process-wide static state. For application-owned route sources, use Router::setup() to clear previous registration and load the configured sources/cache. Do not call it after the inline registration above: it clears those routes before loading its configured sources.
Framework configuration
Register Lsr\Core\Routing\DI\RoutingExtension in the application's Nette DI configuration. Its schema and service definitions expose:
routeFiles: PHP files or directories containing route declarations. Route files are included in router instance scope and can register through$this.controllers: controller files or directories scanned for route attributes fromAttributes.cache.file,cache.autoCompile,cache.checkTimestampsandcache.commands: compiled cache configuration. Timestamp checking defaults tofalse, so plan explicit cache invalidation/recompilation when deploying changed routes. The default cache file isTMP_DIR . 'routes.php', or aroutes.phpfile under the system temporary directory'slsrsubdirectory whenTMP_DIRis not defined.sitemap.defaultIncluded: whether routes are included in sitemap discovery by default; it defaults tofalse. Route metadata and sitemap declarations are discovery information, not access control. SeeSitemap.
Middleware can be attached to routes/groups. Named middleware groups and serviceRef() allow service-backed middleware; the DI extension supplies a Nette service resolver. Resolve configured route dependencies through setup() before dispatch. See RouteGroup and NetteServiceResolver for integration contracts.
RouteGroup::param() keeps service-reference validation deferred for built-in Route instances. If a custom group supplies another RouteInterface implementation, it resolves service references before calling that route's validator-only param() contract; raw references are not forwarded as validators. The base interface is unchanged.
Exact-host routing
Version 0.5 adds optional domain constraints without changing RouteInterface or the unrestricted route table. domain() creates a RouteGroup, so there is no need to call group() first:
$this->domain('public') ->get('/', [PublicController::class, 'index']) ->name('public.home'); $this->domain('admin') ->middlewareAll('admin-authentication') ->get('/', [AdminController::class, 'index']) ->name('admin.home'); // Declarations may follow their use, including in a later route file. $this->declareDomain('www.example.com', alias: 'public'); $this->declareDomain('admin.example.com', alias: 'admin'); // No constraint: intentionally available on every host. $this->get('/health', [HealthController::class, 'show']);
The example assumes the application also declares the admin-authentication middleware group. A domain constraint is not authentication or authorization.
domain('admin')->group('/api') and group('/api')->domain('admin') both produce an /api group restricted to the admin host. On a group, domain() creates a child with the same path prefix and inherited settings; existing routes and sibling groups are not changed. Ordinary children inherit the host constraint, while another domain() child explicitly selects its own host.
Aliases and setup
After all controller attributes and route files load, each domain reference receives one alias lookup. If the alias exists, its concrete hostname replaces the reference; otherwise, the original string is treated as the hostname. Aliases can themselves look like hostnames. Unregistered strings are not missing-alias errors, and alias targets are not recursively expanded.
Aliases and literal hostnames resolving to the same host contribute to the same routing tree. Duplicate routes are checked after resolution using their final host, method and path; group middleware remains attached to its own routes. Conflicting declarations for one alias throw InvalidArgumentException; repeating the same normalized mapping is allowed before resolution. Alias names use exact string matching.
setup() and loadRoutes() finalize domain constraints automatically, after middleware dependency resolution. For inline registration, call $router->resolveDomains() after the last declaration, before matching by host, generating domain-bound links or discovering domain-bound sitemap routes. Host-aware matching refuses pending declarations rather than exposing them as unrestricted routes. Alias declarations are frozen after resolution; new routes may still use existing aliases. unregisterAll() resets domain tables, aliases and resolution state.
Concrete hosts are case-insensitive; a single terminal DNS dot is ignored. ASCII DNS/punycode names, single-label hosts such as localhost, IPv4 and IPv6 literals are supported. Schemes, ports, paths, user information and wildcards are not hostname constraints. Convert internationalized names to punycode before configuration.
Matching and HTTP behavior
$params = []; $route = Router::getRoute( RequestMethod::GET, ['users', '42'], $params, host: $request->getUri()->getHost(), );
The optional fifth argument preserves existing calls, including the fourth argument used for an explicit route tree. Without a host, only unrestricted routes are eligible. With a host, matching tries that host's tree first, then unrestricted routes as a per-method fallback. Parameters from unsuccessful candidates never leak into the selected route.
Domain-only paths on the wrong host are absent, not redirects. A method mismatch produces MethodNotAllowedException only if the path exists in an applicable tree. Automatic HEAD stays within the selected GET route's host; a host-specific GET fallback precedes an unrestricted HEAD route. Explicit OPTIONS handlers take priority over synthesis; synthetic OPTIONS, including OPTIONS *, combines methods from the current host and unrestricted routes only.
Route names remain globally unique across hosts. Localized paths share their logical route's domain. redirectFrom() inherits that domain; an explicitly registered redirect alias can instead have its own source domain and redirect to a different destination host, preserving the request scheme, port and query.
Attributes, cache and discovery
#[Domain('admin')] can constrain a controller class or method. Method declarations override class declarations; a route attribute's optional trailing domain: argument overrides both:
use Lsr\Core\Routing\Attributes\Domain; use Lsr\Core\Routing\Attributes\Get; use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; #[Domain('admin')] class AdminController { #[Get('/dashboard', name: 'admin.dashboard')] public function dashboard(): ResponseInterface { return new Response(200, [], 'Admin dashboard'); } #[Get('/status', name: 'public.status', domain: 'public')] public function status(): ResponseInterface { return new Response(200, [], 'Public status'); } }
The attributes are under Lsr\Core\Routing\Attributes. They use the same deferred alias resolution as fluent groups.
Router::$availableRoutes and getAvailableRoutes() retain their unrestricted tree shape. getDomainRoutes() exposes the resolved per-host trees separately. Built-in routes implement the optional DomainRouteInterface::getDomain() capability; custom implementations of the existing RouteInterface remain valid. Sitemap discovery still returns route objects, whose resolved domains are available for application-owned URL generation and host filtering; domain restrictions do not implicitly include or exclude routes from sitemaps.
Compiled cache format 4 stores resolved host constraints, domain trees and aliases. Older formats are rejected and rebuilt from configured sources. Rebuild the route cache whenever domain configuration changes, including environment-derived alias targets; warm loading does not execute route declarations again. Keep caches deployment-specific and restart long-running workers after route changes.
lsr/core 0.5 forwards the request host and generates domain-aware named links and redirects. Routing does not validate a global host allowlist or trust forwarded headers; configure accepted hosts and trusted proxies at the application/web-server layer.
Development
GitHub Actions runs CS, PHPStan and PHPUnit on PHP 8.4 and 8.5. Run the same checks locally after installing development dependencies:
composer install --prefer-dist --no-interaction --no-progress composer cs vendor/bin/phpstan analyse --no-progress vendor/bin/phpunit --no-coverage
Configuration is in phpstan.neon and phpunit.xml. CI does not require a coverage driver or external services. composer test remains available for coverage runs with a compatible driver.
Run composer cs to check PHP coding style and composer cs:fix (or composer cbf) to apply fixes with PHP CS Fixer. The rules and source paths are defined in .php-cs-fixer.php.
AI coding assistance
See LSR Skills for AI agent skills for working with the LSR framework.
License
Licensed under the MIT License.