HTTP request router.
Contents
- defining routes using OO builders
- matching request attributes (method, scheme, host, port, path)
- regex-driven host and path patterns
- generating URLs
- PHP 7.1+
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";
}
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";
}
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');
// ...
});
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
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}
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}
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.\-]+']);
});
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.
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).
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 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 $
.
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
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.
After routes have been defined, the router can be used to route a request.
See example code in Routing incoming requests.
Both Router->match()
and Router->matchPath()
return an instance of Kuria\Router\Result\Result
,
which may be one of the following:
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"}
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()));
}
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);
}
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.
After routes have been defined, the router can be used to generate URLs.
See Routing incoming requests for an example of a configured router.
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 is used to fill in missing information (scheme, host, port, etc.) when generating URLs or matching paths.
It can be specified in two ways:
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
));
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.