kuria / router
HTTP request router
Requires
- php: >=7.1
- kuria/url: ^5.0
Requires (Dev)
- ext-json: *
- kuria/dev-meta: ^0.6
This package is auto-updated.
Last update: 2024-10-22 18:00:13 UTC
README
HTTP request router.
Contents
- Features
- Requirements
- Usage
Features
- defining routes using OO builders
- matching request attributes (method, scheme, host, port, path)
- regex-driven host and path patterns
- generating URLs
Requirements
- PHP 7.1+
Usage
Routing incoming requests
Simple PATH_INFO
routing
Simple routing using $_SERVER['PATH_INFO']
and hardcoded context information.
Example URL: http://localhost/index.php/page/index
<?php use Kuria\Router\Context; use Kuria\Router\Result\Match; use Kuria\Router\Result\MethodNotAllowed; use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; // create router $router = new Router(); // define default context $router->setDefaultContext(new Context( 'http', // scheme 'localhost', // host 80, // port '/index.php' // base path )); // define routes $router->defineRoutes(function (RouteCollector $c) { $c->get('index')->path('/'); $c->get('page')->path('/page/{name}'); $c->addGroup('user_', '/user', function (RouteCollector $c) { $c->add('register')->methods(['GET', 'POST'])->path('/register'); $c->add('login')->methods(['GET', 'POST'])->path('/login'); $c->get('logout')->path('/logout'); $c->get('profile')->path('/profile/{username}'); }); }); // match current request $path = rawurldecode($_SERVER['PATH_INFO'] ?? '/'); $result = $router->matchPath($_SERVER['REQUEST_METHOD'], $path); // handle the result if ($result instanceof Match) { // success // do something with the matched route and parameters echo 'Matched path: ', $result->getSubject()->path, "\n"; echo 'Matched route: ', $result->getRoute()->getName(), "\n"; echo 'Parameters: ', print_r($result->getParameters(), true), "\n"; } elseif ($result instanceof MethodNotAllowed) { // method not allowed http_response_code(405); header('Allow: ' . implode(', ', $result->getAllowedMethods())); echo "Method not allowed :(\n"; } else { // not found http_response_code(404); echo "Not found :(\n"; }
Dynamic routing using kuria/request-info
Context and path info can be auto-detected using the kuria/request-info library.
It supports both simple path info and rewritten URLs and can extract information from trusted proxy headers.
<?php use Kuria\RequestInfo\RequestInfo; use Kuria\Router\Context; use Kuria\Router\Result\Match; use Kuria\Router\Result\MethodNotAllowed; use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; // create router $router = new Router(); // define default context $router->setDefaultContext(new Context( RequestInfo::getScheme(), RequestInfo::getHost(), RequestInfo::getPort(), RequestInfo::getBasePath() )); // define routes $router->defineRoutes(function (RouteCollector $c) { $c->get('index')->path('/'); $c->get('page')->path('/page/{name}'); $c->addGroup('user_', '/user', function (RouteCollector $c) { $c->add('register')->methods(['GET', 'POST'])->path('/register'); $c->add('login')->methods(['GET', 'POST'])->path('/login'); $c->get('logout')->path('/logout'); $c->get('profile')->path('/profile/{username}'); }); }); // match current request $path = rawurldecode(RequestInfo::getPathInfo()); $result = $router->matchPath(RequestInfo::getMethod(), $path !== '' ? $path : '/'); // handle the result if ($result instanceof Match) { // success // do something with the matched route and parameters echo 'Matched path: ', $result->getSubject()->path, "\n"; echo 'Matched route: ', $result->getRoute()->getName(), "\n"; echo 'Parameters: ', print_r($result->getParameters(), true), "\n"; } elseif ($result instanceof MethodNotAllowed) { // method not allowed http_response_code(405); header('Allow: ' . implode(', ', $result->getAllowedMethods())); echo "Method not allowed :(\n"; } else { // not found http_response_code(404); echo "Not found :(\n"; }
Defining routes
RouteCollector
provides a convenient interface to define routes.
The easier way to use it is to call Router->defineRoutes()
with a callback
accepting an instance of RouteCollector
. The router then takes care of adding
the defined routes.
<?php use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; $router = new Router(); $router->defineRoutes(function (RouteCollector $c) { $c->get('index')->path('/'); $c->post('login')->path('/login'); // ... });
Route collector API
RouteCollector
provides methods to create and organize route builders.
The returned RouteBuilder
instances can be used to configure the routes.
See Route builder API.
add($routeName): RouteBuilder
- add a routeget($routeName): RouteBuilder
- add a route that matches GET requestshead($routeName): RouteBuilder
- add a route that matches HEAD requestspost($routeName): RouteBuilder
- add a route that matches POST requestsput($routeName): RouteBuilder
- add a route that matches PUT requestsdelete($routeName): RouteBuilder
- add a route that matches DELETE requestsoptions($routeName): RouteBuilder
- add a route that matches OPTIONS requestspatch($routeName): RouteBuilder
- add a route that matches PATCH requestsaddVariant($existingRouteName, $newRouteName): RouteBuilder
- add a variant of an existing route, see Route variantsaddGroup($namePrefix, $pathPrefix, $callback): void
- add a group of routes with common prefixes, see Route groupshasBuilder($routeName): bool
- see if a route is definedgetBuilder($routeName): RouteBuilder
- get builder for the given routeremoveBuilder($routeName): void
- remove route definitiongetBuilders(): RouteBuilder[]
- get all configured buildersgetRoutes(): Route[]
- build routesclear(): void
- remove all defined routes
Route variants
To add multiple similar routes, you can define a single route and then use that
definition as a base of new routes by calling addVariant()
:
<?php use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; $router = new Router(); $router->defineRoutes(function (RouteCollector $c) { // define a base route $c->get('get_row') ->path('/{database}/{row}') ->defaults(['format' => 'json']); // define a variant of the base route $c->addVariant('get_row', 'get_row_with_format') ->appendPath('.{format}') ->requirements(['format' => 'json|xml']); }); // print defined routes foreach ($router->getRoutes() as $route) { echo $route->getName(), ' :: ', $route->dump(), "\n"; }
Output:
get_row :: GET /{database}/{row} get_row_with_format :: GET /{database}/{row}.{format}
Route groups
To define several routes that share a common path and name prefix, use addGroup()
:
<?php use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; $router = new Router(); $router->defineRoutes(function (RouteCollector $c) { $c->addGroup('user_', '/user', function (RouteCollector $c) { $c->add('register')->methods(['GET', 'POST'])->path('/register'); $c->add('login')->methods(['GET', 'POST'])->path('/login'); $c->get('logout')->path('/logout'); $c->get('profile')->path('/profile/{username}'); }); }); // print defined routes foreach ($router->getRoutes() as $route) { echo $route->getName(), ' :: ', $route->dump(), "\n"; }
Output:
user_register :: GET|POST /user/register user_login :: GET|POST /user/login user_logout :: GET /user/logout user_profile :: GET /user/profile/{username}
Route builder API
RouteBuilder
provides a fluent interface to configure a single route.
methods($allowedMethods): self
- match request methods (must be uppercase, e.g.GET
,POST
, etc.)scheme($scheme): self
- match a scheme (e.g.http
orhttps
)host($hostPattern): self
- match host name pattern, see Route patternsprependHost($hostPatternPrefix): self
- add a prefix to the host name patternappendHost($hostPatternPrefix): self
- add a suffix to the host name patternport($port): self
- match portpath($pathPattern): self
- match path pattern, see Route patternsprependPath($pathPatternPrefix): self
- add a prefix to the path patternappendPath($pathPatternPrefix): self
- add a suffix to the path patterndefaults($defaults): self
- specify default parameters, see Route defaultsattributes($attributes): self
- specify arbitrary route attributes, see Route attributesrequirements($requirements): self
- specify parameter requirements, see Route requirements
Example call:
<?php $router->defineRoutes(function (RouteCollector $c) { // $c->add() returns a RouteBuilder $c->add('user_profile_page') ->methods(['GET', 'POST']) ->scheme('https') ->host('{username}.example.com') ->port(8080) ->path('/{page}') ->defaults(['page' => 'home']) ->requirements(['username' => '\w+', 'page' => '[\w.\-]+']); });
Route patterns
The host and path of a route can contain any number of parameter placeholders.
Placeholder syntax is the following:
{parameterName}
Parameter name can consist of any characters with the exception of }
.
These parameters will be available in the matching result. See Matching routes.
Note
Optional pattern parameters are not supported. If you need differently structured URLs to match the same resource, define multiple routes accordingly.
See Route variants.
Route defaults
A route can contain default parameter values.
These defaults are used when generating URLs (in case one or more parameters haven't been specified). See Generating URLs.
Default parameters can also be useful when defining multiple routes that point to the same resource (so the routes are interchangeable).
Route attributes
A route can contain arbitrary attributes.
The use depends entirely on the application, but it is a good place to store various metadata, e.g. controller names or handler callables.
Route requirements
Route requirements are a set of plain regular expressions for each host or path pattern parameter. See Route patterns.
The regular expressions should not be delimited. They are also anchored automatically, so
they should not contain ^
or $
.
Default requirements
If no requirement is specified, a default one will be assumed instead, depending on the type of the pattern:
- host pattern:
.+
- one or more characters of any type
- path pattern:
[^/]+
- one or more characters that are not a forward slash
Caching routes
Building and compiling routes will introduce some overhead into your application. Luckily, the defined routes can be serialized and stored for later use.
Below is an example of route caching using the kuria/cache library, but you can any other library or code.
<?php use Kuria\Cache\Cache; use Kuria\Cache\Driver\Filesystem\FilesystemDriver; use Kuria\Router\Route\RouteCollector; use Kuria\Router\Router; // example cache $cache = new Cache(new FilesystemDriver(__DIR__ . '/cache')); // create router $router = new Router(); // attempt to load routes from the cache $routes = $cache->get('routes'); if ($routes === null) { // no routes found in cache, define them $router->defineRoutes(function (RouteCollector $c) { $c->get('index')->path('/'); $c->get('page')->path('/page/{name}'); }); // store defined routes in the cache $cache->set('routes', $router->getRoutes()); } else { // use routes from cache $router->setRoutes($routes); }
Note
Routes that contain unserializable values (such as closures in the attributes) cannot be cached.
Matching routes
After routes have been defined, the router can be used to route a request.
See example code in Routing incoming requests.
Using Router->match()/matchPath()
Both Router->match()
and Router->matchPath()
return an instance of Kuria\Router\Result\Result
,
which may be one of the following:
Kuria\Router\Result\Match
A route has been matched successfully. The Match
object provides access to the
matched route and parameters.
It us up to the application to do something with this information.
<?php use Kuria\Router\Result\Match; use Kuria\Router\Router; $result = $router->matchPath('GET', '/user/profile/bob'); if ($result instanceof Match) { echo 'Matched route is ', $result->getRoute()->getName(), "\n"; echo 'Matched parameters are: ', json_encode($result->getParameters()), "\n"; }
Output:
Matched route is user_profile Matched parameters are: {"username":"bob"}
Tip
You can access route attributes at $result->getRoute()->getAttributes()
.
See Route attributes.
Kuria\Router\Result\MethodNotAllowed
No routes have been matched, but there are routes that would match if the method was different.
A proper response in this case is HTTP 405 Method Not Allowed, with an Allow
header specifying the allowed methods.
<?php use Kuria\Router\Result\MethodNotAllowed; $result = $router->matchPath('POST', '/user/logout'); if ($result instanceof MethodNotAllowed) { http_response_code(405); header('Allow: ' . implode(', ', $result->getAllowedMethods())); }
Kuria\Router\Result\NotFound
No routes have matched.
A proper response in this case is HTTP 404 Not Found.
<?php use Kuria\Router\Result\NotFound; $result = $router->matchPath('GET', '/nonexistent'); if ($result instanceof NotFound) { http_response_code(404); }
HEAD to GET fallback
To ease compliance with the HTTP specification, if a HEAD
request does not match
any route, a second matching attempt will be made assuming a GET
method instead.
PHP itself supports HEAD
requests and will only respond with headers, so you don't
have to craft additional routes to handle these requests in most cases.
Generating URLs
After routes have been defined, the router can be used to generate URLs.
See Routing incoming requests for an example of a configured router.
Using Router->generate()
The Router->generate()
method will generate an URL for the given route
and parameters and return an instance of Kuria\Url\Url
.
- if no such route exists or the parameters are invalid, an exception will be thrown (see Route requirements)
- if some parameters are missing, the configured default values will be used instead (see Route defaults)
- any extra parameters (that are not present in the host or path pattern) will be added as query parameters instead
- if the scheme, host or port is different from the context, the URL's preferred
format will be
Url::ABSOLUTE
; if they are all the same or undefined, it will beUrl::RELATIVE
(See Router context)
<?php var_dump( $router->generate('user_register')->build(), $router->generate('user_profile', ['username' => 'bob', 'extra' => 'example'])->build() );
Output:
string(14) "/user/register" string(31) "/user/profile/bob?extra=example"
If you wish to get absolute URLs regardless of the context, use buildAbsolute()
:
<?php var_dump( $router->generate('index')->buildAbsolute(), $router->generate('page', ['name' => 'contact'])->buildAbsolute() );
Output:
string(17) "http://localhost/" string(29) "http://localhost/page/contact"
Router context
Router context is used to fill in missing information (scheme, host, port, etc.) when generating URLs or matching paths.
It can be specified in two ways:
Using Router->setDefaultContext()
This method defines a default context to be used the none is given.
<?php use Kuria\Router\Context; $router->setDefaultContext(new Context( 'https', // scheme 'example.com', // host 443, // port '' // basePath ));
Using the $context
parameter
Router->matchPath()
and Router->generate()
accept an optional $context
argument.
If no context is given, the default context will be used instead. If no default context is specified, an exception will be thrown.