divineniiquaye/flight-routing

Flight routing is a simple, fast PHP router that is easy to get integrated with other routers.

v2.1.0 2022-12-07 21:55 UTC

README

PHP Version Latest Version Workflow Status Code Maintainability Coverage Status Psalm Type Coverage Quality Score

Flight routing is yet another high performance HTTP router for PHP. It is simple, easy to use, scalable and fast. This library depends on PSR-7 for route match and support using PSR-15 for intercepting route before being rendered.

This library previous versions was inspired by Sunrise Http Router, Symfony Routing, FastRoute and now completely rewritten for better performance.

🏆 Features

  • Supports all HTTP request methods (eg. GET, POST DELETE, etc).
  • Regex Expression constraints for parameters.
  • Reversing named routes paths to full URL with strict parameter checking.
  • Route grouping and merging.
  • Supports routes caching for performance.
  • PSR-15 Middleware (classes that intercepts before the route is rendered).
  • Domain and sub-domain routing.
  • Restful Routing.
  • Supports PHP 8 attribute #[Route] and doctrine annotation @Route routing.
  • Support custom matching strategy using custom route matcher class or compiler class.

📦 Installation

This project requires PHP 8.0 or higher. The recommended way to install, is via Composer. Simply run:

$ composer require divineniiquaye/flight-routing

I recommend reading my blog post on setting up Apache, Nginx, IIS server configuration for your PHP project.

📍 Quick Start

The default compiler accepts the following constraints in route pattern:

  • {name} - required placeholder.
  • {name=foo} - placeholder with default value.
  • {name:regex} - placeholder with regex definition.
  • {name:regex=foo} - placeholder with regex definition and default value.
  • [{name}] - optional placeholder.

A name of a placeholder variable is simply an acceptable PHP function/method parameter name expected to be unique, while the regex definition and default value can be any string (i.e [^/]+).

  • /foo/ - Matches /foo/ or /foo. ending trailing slashes are striped before matching.
  • /user/{id} - Matches /user/bob, /user/1234 or /user/23/.
  • /user/{id:[^/]+} - Same as the previous example.
  • /user[/{id}] - Same as the previous example, but also match /user or /user/.
  • /user[/{id}]/ - Same as the previous example, ending trailing slashes are striped before matching.
  • /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.
  • /[{lang:[a-z]{2}}[-{sublang}]/]{name}[/page-{page=0}] - Matches /cs/hello, /en-us/hello, /hello, /hello/page-12, or /ru/hello/page-23

Route pattern accepts beginning with a //domain.com or https://domain.com. Route path also support adding controller (i.e *<controller@handler>) directly at the end of the route path:

  • *<App\Controller\BlogController@indexAction> - translates as a callable of BlogController class with method named indexAction.
  • *<phpinfo> - translates as a function, if a handler class is defined in route, then it turns to a callable.

Here is an example of how to use the library:

use Flight\Routing\{Router, RouteCollection};

$router = new Router();
$router->setCollection(static function (RouteCollection $routes) {
    $routes->add('/blog/[{slug}]', handler: [BlogController::class, 'indexAction'])->bind('blog_show');
    //... You can add more routes here.
});

Incase you'll prefer declaring your routes outside a closure scope, try this example:

use Flight\Routing\{Router, RouteCollection};

$routes = new RouteCollection();
$routes->get('/blog/{slug}*<indexAction>', handler: BlogController::class)->bind('blog_show');

$router = Router::withCollection($routes);

NB: If caching is enabled, using the router's setCollection() method has much higher performance than using the withCollection() method.

By default Flight routing does not ship a PSR-7 http library nor a library to send response headers and body to the browser. If you'll like to install this libraries, I recommend installing either biurad/http-galaxy or nyholm/psr7 and laminas/laminas-httphandlerrunner.

$request = ... // A PSR-7 server request initialized from global request

// Routing can match routes with incoming request
$route = $router->matchRequest($request);
// Should return an array, if request is made on a a configured route path (i.e /blog/lorem-ipsum)

// Routing can also generate URLs for a given route
$url = $router->generateUri('blog_show', ['slug' => 'my-blog-post']);
// $url = '/blog/my-blog-post' if stringified else return a GeneratedUri class object

In this example below, I'll assume you've installed nyholm/psr-7 and laminas/laminas-httphandlerrunner, So we can use PSR-15 to intercept route before matching and PSR-17 to render route response onto the browser:

use Flight\Routing\Handlers\RouteHandler;
use Laminas\HttpHandlerRunner\Emitter\SapiStreamEmitter;

$router->pipe(...); # Add PSR-15 middlewares ...

$handlerResolver = ... // You can add your own route handler resolver else default is null
$responseFactory = ... // Add PSR-17 response factory
$request = ... // A PSR-7 server request initialized from global request

// Default route handler, a custom request route handler can be used also.
$handler = new RouteHandler($responseFactory, $handlerResolver);

// Match routes with incoming request and return a response
$response = $router->process($request, $handler);

// Send response to the browser ...
(new SapiStreamEmitter())->emit($response);

To use PHP 8 attribute support, I highly recommend installing biurad/annotations and if for some reason you decide to use doctrine/annotations I recommend you install spiral/attributes to use either one or both.

An example using annotations/attribute is:

use Biurad\Annotations\AnnotationLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Flight\Routing\Annotation\Listener;
use Spiral\Attributes\{AnnotationReader, AttributeReader};
use Spiral\Attributes\Composite\MergeReader;

$reader = new AttributeReader();

// If you only want to use PHP 8 attribute support, you can skip this step and set reader to null.
if (\class_exists(AnnotationRegistry::class)) $reader = new MergeReader([new AnnotationReader(), $reader]);

$loader = new AnnotationLoader($reader);
$loader->listener(new Listener(), 'my_routes');
$loader->resource('src/Controller', 'src/Bundle/BundleName/Controller');

$annotation = $loader->load('my_routes'); // Returns a Flight\Routing\RouteCollection class instance

You can add more listeners to the annotation loader class to have all your annotations/attributes loaded from one place. Also use either the populate() route collection method or group() to merge annotation's route collection into default route collection, or just simple use the annotation's route collection as your default router route collection.

Finally, use a restful route, refer to this example below, using Flight\Routing\RouteCollection::resource, method means, route becomes available for all standard request methods Flight\Routing\Router::HTTP_METHODS_STANDARD:

namespace Demo\Controller;

class UserController {
    public function getUser(int $id): string {
        return "get {$id}";
    }

    public function postUser(int $id): string {
        return "post {$id}";
    }

    public function deleteUser(int $id): string {
        return "delete {$id}";
    }
}

Add route using Flight\Routing\Handlers\ResourceHandler:

use Flight\Routing\Handlers\ResourceHandler;

$routes->add('/user/{id:\d+}', ['GET', 'POST'], new ResourceHandler(Demo\UserController::class, 'user'));

As of Version 2.0, flight routing is very much stable and can be used in production, Feel free to contribute to the project, report bugs, request features and so on.

Kindly take note of these before using:

  • Avoid declaring the same pattern of dynamic route multiple times (eg. /hello/{name}), instead use static paths if you choose use same route path with multiple configurations.
  • Route handlers prefixed with a \ (eg. \HelloClass or ['\HelloClass', 'handle']) should be avoided if you choose to use a different resolver other the default handler's RouteInvoker class.
  • If you decide again to use a custom route's handler resolver, I recommend you include the static resolveRoute method from the default's route's RouteInvoker class.

📓 Documentation

In-depth documentation on how to use this library, kindly check out the documentation for this library. It is also recommended to browse through unit tests in the tests directory.

🙌 Sponsors

If this library made it into your project, or you interested in supporting us, please consider donating to support future development.

👥 Credits & Acknowledgements

📄 License

Flight Routing is completely free and released under the BSD 3 License.