Awesome
Router - HTTP Request Router
Complete benchmark results can be found here.
- Compatible with PSR-7
- Named routes
- Reverses routing
- Sub-domain
- Nested routes
- Custom dispatching strategy
- Advanced route pattern syntax
Installation
composer require crysalead/router
API
Route patterns
Route pattern are path string with curly brace placeholders. Possible placeholder format are:
'{name}'
- placeholder'{name:regex}'
- placeholder with regex definition.'[{name}]'
- optionnal placeholder'[{name}]+'
- recurring placeholder'[{name}]*'
- optionnal recurring placeholder
Variable placeholders may contain only word characters (latin letters, digits, and underscore) and must be unique within the pattern. For placeholders without an explicit regex, a variable placeholder matches any number of characters other than '/' (i.e [^/]+
).
You can use square brackets (i.e []
) to make parts of the pattern optional. For example /foo[bar]
will match both /foo
and /foobar
. Optional parts can be nested and repeatable using the []*
or []+
syntax. Example: /{controller}[/{action}[/{args}]*]
.
Examples:
'/foo/'
- Matches only if the path is exactly '/foo/'. There is no special treatment for trailing slashes, and patterns have to match the entire path, not just a prefix.'/user/{id}'
- Matches '/user/bob' or '/user/1234!!!' or even '/user/bob/details' but not '/user/' or '/user'.'/user/{id:[^/]+}'
- Same as the previous example.'/user[/{id}]'
- Same as the previous example, but also match '/user'.'/user[/[{id}]]'
- Same as the previous example, but also match '/user/'.'/user[/{id}]*'
- Match '/user' as well as 'user/12/34/56'.'/user/{id:[0-9a-fA-F]{1,8}}'
- Only matches if the id parameter consists of 1 to 8 hex digits.'/files/{path:.*}'
- Matches any URL starting with '/files/' and captures the rest of the path into the parameter 'path'.
Note: the difference between /{controller}[/{action}[/{args}]*]
and /{controller}[/{action}[/{args:.*}]]
for example is args
will be an array using [/{args}]*
while a unique "slashed" string using [/{args:.*}]
.
The Router
The Router
instance can be instantiated so:
use Lead\Router\Router;
$router = new Router();
Optionally, if your project lives in a sub-folder of your web root you'll need to set a base path using basePath()
. This base path will be ignored so your routes won't need to be prefixed with it to matches the request path.
$router->basePath('/my/sub/dir');
Note: If you are using the crysalead/net library you can pass Request::ingoing()->basePath();
directly so you won't need to set it manually.
The Router Public Methods
$router->basePath(); // Gets/sets the router base path
$router->group(); // To create some scoped routes
$router->bind(); // To create a route
$router->route(); // To route a request
$router->link(); // To generate a route's link
$router->apply(); // To add a global middleware
$router->middleware(); // The router's middleware generator
$router->strategy(); // Gets/sets a routing strategy
Route definition
Example of routes definition:
use Lead\Router\Router;
$router = new Router();
$router->bind($pattern, $handler); // route matching any request method
$router->bind($pattern, $options, $handler); // alternative syntax with some options.
$router->bind($pattern, ['methods' => 'GET'], $handler); // route matching on only GET requests
$router->bind($pattern, ['methods' => ['POST', 'PUT']], $handler); // route matching on POST and PUT requests
// Alternative syntax
$router->get($pattern, $handler); // route matching only get requests
$router->post($pattern, $handler); // route matching only post requests
$router->delete($pattern, $handler); // route matching only delete requests
In the above example a route is registered using the ->bind()
method and takes as parametters a route pattern, an optionnal options array and the callback handler.
The second parameter is an $options
array where possible values are:
'scheme'
: the scheme constraint (default:'*'
)'host'
: the host constraint (default:'*'
)'methods'
: the method constraint (default:'*'
)'name'
: the name of the route (optional)'namespace'
: the namespace to attach to a route (optional)
The last parameter is the callback handler which contain the dispatching logic to execute when a route matches the request. The callback handler is the called with the matched route as first parameter and the response object as second parameter:
$router->bind('foo/bar', function($route, $response) {
});
The Route Public Attributes
$route->method; // The method contraint
$route->params; // The matched params
$route->persist; // The persisted params
$route->namespace; // The namespace
$route->name; // The route's name
$route->request; // The routed request
$route->response; // The response (same as 2nd argument, can be `null`)
$route->dispatched; // To store the dispated instance if applicable.
The Route Mublic Methods
$route->host(); // The route's host instance
$route->pattern(); // The pattern
$route->regex(); // The regex
$route->variables(); // The variables
$route->token(); // The route's pattern token structure
$route->scope(); // The route's scope
$route->error(); // The route's error number
$route->message(); // The route's error message
$route->link(); // The route's link
$route->apply(); // To add a new middleware
$route->middleware(); // The route's middleware generator
$route->handler(); // The route's handler
$route->dispatch(); // To dispatch the route (i.e execute the route's handler)
Named Routes And Reverse Routing
To be able to do some reverse routing, route must be named using the following syntax first:
$route = $router->bind('foo/{bar}', ['name' => 'foo'], function() { return 'hello'; });
Named routes can be retrieved using the array syntax on the router instance:
$router['foo']; // Returns the `'foo'` route.
Once named, the reverse routing can be done using the ->link()
method:
echo $router->link('foo', ['bar' => 'baz']); // /foo/baz
The ->link()
method takes as first parameter the name of a route and as second parameter the route's arguments.
Grouping Routes
It's possible to apply a scope to a set of routes all together by grouping them into a dedicated group using the ->group()
method.
$router->group('admin', ['namespace' => 'App\Admin\Controller'], function($router) {
$router->bind('{controller}[/{action}]', function($route, $response) {
$controller = $route->namespace . ucfirst($route->params['controller']);
$instance = new $controller($route->params, $route->request, $route->response);
$action = isset($route->params['action']) ? $route->params['action'] : 'index';
$instance->{$action}();
return $route->response;
});
});
The above example will be able to route /admin/user/edit
on App\Admin\Controller\User::edit()
. The fully-namespaced class name of the controller is built using the {controller}
variable and it's then instanciated to process the request by running the {action}
method.
Sub-Domain And/Or Prefix Routing
To supports some sub-domains routing, the easiest way is to group routes using the ->group()
method and setting up the host constraint like so:
$router->group(['host' => 'foo.{domain}.bar'], function($router) {
$router->group('admin', function($router) {
$router->bind('{controller}[/{action}]', function() {});
});
});
The above example will be able to route http://foo.hello.bar/admin/user/edit
for example.
Middleware
Middleware functions are functions that have access to the request object, the response object, and the next middleware function in the application’s request-response cycle. Middleware functions provide the same level of control as aspects in AOP. It allows to:
- Execute any code.
- Make changes to the request and the response objects.
- End the request-response cycle.
- Call the next middleware function in the stack.
And it's also possible to apply middleware functions globally on a single route or on a group of them. Adding a middleware to a Route is done using the ->apply()
method:
$mw = function ($request, $response, $next) {
return 'BEFORE' . $next($request, $response) . 'AFTER';
};
$router->get('foo', function($route) {
return '-FOO-';
})
echo $router->route('foo')->dispatch($response); //BEFORE-FOO-AFTER
You can also attach middlewares on groups.
$mw1 = function ($request, $response, $next) {
return '1' . $next($request, $response) . '1';
};
$mw2 = function ($request, $response, $next) {
return '2' . $next($request, $response) . '2';
};
$mw3 = function ($request, $response, $next) {
return '3' . $next($request, $response) . '3';
};
$router->apply($mw1); // Global
$router->group('foo', function($router) {
$router->get('bar', function($route) {
return '-BAR-';
})->apply($mw3); // Local
})->apply($mw2); // Group
echo $router->route('foo/bar')->dispatch($response); //321-BAR-123
Dispatching
Dispatching is the outermost layer of the framework, responsible for both receiving the initial HTTP request and sending back a response at the end of the request's life cycle.
This step has the responsibility to loads and instantiates the correct controller, resource or class to build a response. Since all this logic depends on the application architecture, the dispatching has been splitted in two steps for being as flexible as possible.
Dispatching A Request
The URL dispatching is done in two steps. First the ->route()
method is called on the router instance to find a route matching the URL. The route accepts as arguments:
- An instance of
Psr\Http\Message\RequestInterface
- An url or path string
- An array containing at least a path entry
- A list of parameters with the following order: path, method, host and scheme
The ->route()
method returns a route (or a "not found" route), then the ->dispatch()
method will execute the dispatching logic contained in the route handler (or throwing an exception for non valid routes).
use Lead\Router\Router;
$router = new Router();
// Bind to all methods
$router->bind('foo/bar', function() {
return "Hello World!";
});
// Bind to POST and PUT at dev.example.com only
$router->bind('foo/bar/edit', ['methods' => ['POST',' PUT'], 'host' => 'dev.example.com'], function() {
return "Hello World!!";
});
// The Router class makes no assumption of the ingoing request, so you have to pass
// uri, methods, host, and protocol into `->route()` or use a PSR-7 Compatible Request.
// Do not rely on $_SERVER, you must check or sanitize it!
$route = $router->route(
$_SERVER['REQUEST_URI'], // foo/bar
$_SERVER['REQUEST_METHOD'], // get, post, put...etc
$_SERVER['HTTP_HOST'], // www.example.com
$_SERVER['SERVER_PROTOCOL'] // http or https
);
echo $route->dispatch(); // Can throw an exception if the route is not valid.
Dispatching A Request Using Some PSR-7 Compatible Request/Response
It also possible to use compatible Request/Response instance for the dispatching.
use Lead\Router\Router;
use Lead\Net\Http\Cgi\Request;
use Lead\Net\Http\Response;
$request = Request::ingoing();
$response = new Response();
$router = new Router();
$router->bind('foo/bar', function($route, $response) {
$response->body("Hello World!");
return $response;
});
$route = $router->route($request);
echo $route->dispatch($response); // Can throw an exception if the route is not valid.
Handling dispatching failures
use Lead\Router\RouterException;
use Lead\Router\Router;
use Lead\Net\Http\Cgi\Request;
use Lead\Net\Http\Response;
$request = Request::ingoing();
$response = new Response();
$router = new Router();
$router->bind('foo/bar', function($route, $response) {
$response->body("Hello World!");
return $response;
});
$route = $router->route($request);
try {
echo $route->dispatch($response);
} catch (RouterException $e) {
http_response_code($e->getCode());
// Or you can use Whoops or whatever to render something
}
Setting up a custom dispatching strategy.
To use your own strategy you need to create it using the ->strategy()
method.
Bellow an example of a RESTful strategy:
use Lead\Router\Router;
use My\Custom\Namespace\ResourceStrategy;
Router::strategy('resource', new ResourceStrategy());
$router = new Router();
$router->resource('Home', ['namespace' => 'App\Resource']);
// Now all the following URL can be routed
$router->route('home');
$router->route('home/123');
$router->route('home/add');
$router->route('home', 'POST');
$router->route('home/123/edit');
$router->route('home/123', 'PATCH');
$router->route('home/123', 'DELETE');
The strategy:
namespace use My\Custom\Namespace;
class ResourceStrategy {
public function __invoke($router, $resource, $options = [])
{
$path = strtolower(strtr(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $resource), '-', '_'));
$router->get($path, $options, function($route) {
return $this->_dispatch($route, $resource, 'index');
});
$router->get($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
return $this->_dispatch($route, $resource, 'show');
});
$router->get($path . '/add', $options, function($route) {
return $this->_dispatch($route, $resource, 'add');
});
$router->post($path, $options, function($route) {
return $this->_dispatch($route, $resource, 'create');
});
$router->get($path . '/{id:[0-9a-f]{24}|[0-9]+}' .'/edit', $options, function($route) {
return $this->_dispatch($route, $resource, 'edit');
});
$router->patch($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
return $this->_dispatch($route, $resource, 'update');
});
$router->delete($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
return $this->_dispatch($route, $resource, 'delete');
});
}
protected function _dispatch($route, $resource, $action)
{
$resource = $route->namespace . $resource . 'Resource';
$instance = new $resource();
return $instance($route->params, $route->request, $route->response);
}
}