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
composer require crysalead/router
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
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.
$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
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) {
});
$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.
$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)
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.
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.
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 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 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.
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.
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.
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
}
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);
}
}