8ctopus / nano-router
An experimental PSR-7, PSR-17 router
Requires
- php: >=8.1
- psr/http-message: ^1.0|^2.0
Requires (Dev)
- 8ctopus/nano-timer: ^4.0
- filp/whoops: ^2.14
- friendsofphp/php-cs-fixer: ^3.8
- httpsoft/http-emitter: ^1.0
- httpsoft/http-message: ^1.1
- httpsoft/http-server-request: ^1.0
- phpmd/phpmd: ^2.13
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.5|^10.0
This package is auto-updated.
Last update: 2024-11-15 15:34:55 UTC
README
An experimental PSR-7, PSR-17 router
features
- very fast (less than 2ms on simple routing)
- uses PSR-7 and PSR-17 standards
- no dependencies besides PSR-7/17
While I consider it still experimental, I have been using it in production to host legend.octopuslabs.io without any issues so far.
introduction for beginners
The purpose of a router is to match a user (client) http request to a specific function that will handle the user request and deliver a response to the client.
PSR-7 defines the request and response interfaces, while PSR-17 defines the factories for creating them. In other words, factories are used to create the request and response objects.
Here's some pseudo-code that explains the concept:
$router = new Router(); // add route $router->addRoute(new Route(RouteType::Exact, 'GET', '/test.php', function (ServerRequestInterface $request) : ResponseInterface { return new Response(200, ['Content-Type' => 'text/plain'], 'You\'ve reached the test page'); })); // create user http request $request = ServerRequestCreator::createFromGlobals($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST); // resolve finds the function that handles the user request, calls it and returns its response $response = $router->resolve($request); // send response to client (echoes internally) (new SapiEmitter()) ->emit($response);
demo
To play with the demo, clone the repo, run php -S localhost:80 demo/public/index.php -t demo/public/
and open your browser at http://localhost
.
Alternatively you can run the demo within a Docker container docker-compose up &
.
install
-
composer require 8ctopus/nano-router
-
if you don't have any preference for the PSR-7 implementation, install HttpSoft
composer require httpsoft/http-message httpsoft/http-emitter
-
redirect all traffic (except existing files) to the router in
.htaccess
for those using Apache
RewriteEngine on # redirect all not existing files and directories to router RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [END]
and for nginx (untested)
location / { try_files $uri $uri/ /index.php$is_args$args; }
- create
index.php
use Oct8pus\NanoRouter\NanoRouter; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; // use any PSR-7, PSR-17 implementations, here HttpSoft use HttpSoft\Emitter\SapiEmitter; use HttpSoft\Message\Response; use HttpSoft\Message\ServerRequestFactory; use HttpSoft\Message\Stream; use HttpSoft\ServerRequest\ServerRequestCreator; require_once __DIR__ . '/vendor/autoload.php'; $router = new NanoRouter(Response::class, ServerRequestFactory::class); $router // add simple route ->addRoute(new Route(RouteType::Exact, 'GET', '/test.php', function (ServerRequestInterface $request) : ResponseInterface { $stream = new Stream(); $stream->write('test.php'); return new Response(200, [], $stream); })) // add starts with route ->addRoute(new Route(RouteType::StartsWith, ['GET', 'POST'], '/test/', function (ServerRequestInterface $request) : ResponseInterface { $stream = new Stream(); $stream->write('request target - '. $request->getRequestTarget()); return new Response(200, [], $stream); })) // add regex route ->addRoute(new Route(RouteType::Regex, '*', '~/php(.*)/~', function (ServerRequestInterface $request) : ResponseInterface { $stream = new Stream(); $stream->write('request target - '. $request->getRequestTarget()); return new Response(200, [], $stream); })) ->addErrorHandler(404, function (ServerRequestInterface $request) : ResponseInterface { $stream = new Stream(); $stream->write('page not found - ' . $request->getRequestTarget()); return new Response(404, [], $stream); }) ->addMiddleware('*', '~(.*)~', MiddlewareType::Post, function (ResponseInterface $response, ServerRequestInterface $request) : ResponseInterface { return $response->withHeader('X-Powered-By', '8ctopus'); }); // create request from globals $request = ServerRequestCreator::createFromGlobals($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST); // resolve request into a response $response = $router->resolve($request); // send response to client (new SapiEmitter()) ->emit($response);
exception handling
By default, Throwable
, and everything inherited from it such as Exception
, are caught by the router provided the exception occurs within the route code. That functionality can be disabled in the constructor by setting it to false
.
new NanoRouter(Response::class, ServerRequestFactory::class, false, false);
The RouteException
class offers an elegant way to deal with http errors such as 404
, 401
, 429
, .... The router will automatically catch the exception and return the appropriate response to the client.
throw new RouteException('page not found', 404); // client sees 404 page
advanced functionalities
There is more to it, it's just not in the readme yet, most of it can be experimented within the demo, such as:
- pre and post middleware
run tests
composer test
clean code
composer fix(-risky)
todo ideas
- add basePath
- class wrapper for subroutes
- should pre middleware only work on valid requests? now not valid routes are still going through the middleware probably we need both
- add starts with middleware
- check psr-15 middleware
- how to easily route inside class?