crysalead / router
HTTP Request Router
Installs: 16 884
Dependents: 3
Suggesters: 0
Security: 0
Stars: 19
Watchers: 5
Forks: 4
Open Issues: 2
Requires
- php: >=7
- crysalead/collection: ~3.0
- crysalead/net: dev-master
- psr/http-message: ~1.0
Requires (Dev)
- kahlan/kahlan: ~4.6
README
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); } }