fizk / router
A very simple PSR-7 Router
Requires
- php: >=8.1
- psr/http-message: ^1.0
Requires (Dev)
- laminas/laminas-diactoros: ^2.8
- phpunit/phpunit: ^9.5
README
A very simple PRS-7 compatible router.
How it works.
This Router doesn't try to be smart or clever. It doesn't have an opinion on what's it's routing. It doesn't re-write a route expression into RegExp, instead, it wants the expression to be expressed in a Regular Expression upfront. This allows greater control on what will be match against a URI.
This Router is constructed as a tree. This makes the Router that just a little bit faster as it doesn't need to go through a whole list (and every route definition) to find a match.
2 3 4 1How to use it.
Define a Route, give it a name, an Expression and the Parameters you want returned when the Route is a match, then match it against a PSR-7 Request
use Fizk\Router\Route; use Laminas\Diactoros\Request; $routes = new Route( 'root', '/path/(?<id>\d+)', ['handler' => SomeHandler::class] ); $request = new Request('http://this.is/path/1'); $match = $routes->match($request); print_r($match->getAttributes()) // Will print //[ // 'id' => '1' //] print_r($match->getParams()) // Will print //[ // 'handler' => 'Namespace\\SomeHandler' //]
As you can see, there is none of that /path/:id
syntax, instead you need to write the full expression. If you want to capture the Attributes in the URI, you have to give them a name by using Named Captures
Nested routes.
Let's say we have a root path: /path
and then we can have either numbers or letters as Attributes and we want different handlers/controller to run depending on which type Attribute is provided. We can express it like this:
use Fizk\Router\Route; use Laminas\Diactoros\Request; $routes = (new Route('path', '/path', [])) ->addRoute(new Route('letters', '/(?<id>[a-z]+)', ['controller' => SomeLetterController::class])) ->addRoute(new Route('number', '/(?<slug>\d+)', ['controller' => SomeNumberController::class])) ; echo $routes->match(new Request('http://this.is/path/1'))->getParam('handler'); // Will print // Namespace\\SomeNumberController echo $routes->match(new Request('http://this.is/path/arg'))->getParam('handler'); // Will print // Namespace\\SomeLetterController
Routes can be nested "infinitely" deep.
The Router Class.
Defining routes with the ->addRoute(...)
syntax can be a bit verbose. This library provides a class than can take in configuration as an array and build the Router Tree, that way the router configuration is a little bit simpler to manage.
// router.config.php return [ 'base' => [ 'pattern' => '/', 'options' => ['handler' => 'IndexHandler'], ], 'albums' => [ 'pattern' => '/albums', 'options' => ['handler' => 'AlbumsHandler'], 'children' => [ 'album' => [ 'pattern' => '/(?<id>\d+)', 'options' => ['handler' => 'AlbumHandler'], ], ] ], ];
// index.php $router = new Router(require './router.config.php'); $request = new Request('http://example.com/albums/1'); echo $router->match($request)->getParams('handler'); // Will print // AlbumHandler
The array key will become the name of the Route. The required pattern
and options
keys will be passed to the Route instance. An optional children
key can be defined, those routes will become children of the parent route.
Because this class has all the configuration inside of it, it can provide a method called public function construct(string $path, ?array $arguments = []): ?string;
It can construct a URI based off the names you have given to the Routes. An example of this would be:
$config = [ 'index' => [ 'pattern' => '/', 'options' => ['handler' => 'IndexHandler'], ], 'albums' => [ 'pattern' => '/albums', 'options' => ['handler' => 'AlbumsHandler'], 'children' => [ 'album' => [ 'pattern' => '/(?<id>\d+)', 'options' => ['handler' => 'AlbumHandler'], ], ] ], ]; $router = new Router($config); echo $router->construct('albums/album', ['id' => 1]); // This will print // /albums/1
Example
This examples uses Fizk\Router
in conjunction with Psr\Http\Message\ResponseInterface
and Psr\Http\Message\ServerRequestInterface
. What is important to understand is that the Router is not going in inject any values from the URI into the $responce
object or invoke the Controller/Handler. These are things you have to manage on your own.
The benefit of this is that the Router is not dependent on how Controllers/Handlers are implemented or which PSR standard it is using.
use Fizk\Router\Route; use Laminas\Diactoros\Request; use Laminas\Diactoros\Response\JsonResponse; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; //Define Handlers/Controllers class SomeNumberController implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { $id = $request->getAttribute('id'); $data = $service->getById($id); return new JsonResponse($data); } } class SomeLetterController implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { $slug = $request->getAttribute('slug'); $data = $service->getBySlug($slug); return new JsonResponse($slug); } } class ResourceNotFoundController implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { return new JsonResponse(['message' => 'Resource Not Found'], 404); } } // Create a Request Object, pulling CGI values from global scope $request = ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST $_COOKIE, $_FILES ); // Define an Emitter, which will set HTTP headers and body before delivering to client $emitter = new SapiEmitter(); // Define Routes $routes = (new Route('path', '/path', [])) ->addRoute(new Route('letters', '/(?<id>[a-z]+)', ['controller' => new SomeLetterController()])) ->addRoute(new Route('number', '/(?<slug>\d+)', ['controller' => new SomeNumberController])) ; // ...OR USE THE MORE COMPACT WAY OF DEFINING ROUTES $routes = new Router([ 'path' => [ 'pattern' => '/path', 'options' => [] 'children' => [ 'letters' => [ 'pattern' => '/(?<id>[a-z]+)', 'options' => ['controller' => new SomeLetterController()] ], 'numbers' => [ 'pattern' => '/(?<slug>\d+)', 'options' => ['controller' => new SomeNumberController()] ], ] ] ]); // Match Routes against Request Object $match = $routes->match($request); if ($match) { //Add attributes from URI to the Request Object foreach ($match->getAttributes() as $name => $value) { $request = $request->withAttribute($name, $value); } // Run the Handler/Controller $response = $match->getParam('controller')->handle($request); // Emit a Response back to client $emitter->emit($response); } else { // Run the Error Handler/Controller $response = (new ResourceNotFoundController())->handle($request); // Emit a Response back to client $emitter->emit($response); }