Flight routing is a simple, fast PHP router that is easy to get integrated with other routers.
divineniiquaye/flight-routing is a HTTP router for PHP 7.1+ based on PSR-7 and PSR-15 with support for annotations, created by Divine Niiquaye. This library helps create a human friendly urls (also more cool & prettier) while allows you to use any current trends of
PHP Http Routerimplementation and fully meets developers' desires.
GET,
POST,
PUT,
PATCH,
UPDATE,
DELETE) with support for custom multiple verbs.
This project requires PHP 7.1 or higher. The recommended way to install, is via Composer. Simply run:
$ composer require divineniiquaye/flight-routing
First of all, you need to configure your web server to handle all the HTTP requests with a single PHP file like
index.php. Here you can see required configurations for Apache HTTP Server and NGINX.
If you are using Nginx please make sure that url-rewriting is enabled.
You can easily enable url-rewriting by adding the following configuration for the Nginx configuration-file for the demo-project.
location / { try_files $uri $uri/ /index.php?$query_string; }
Nothing special is required for Apache to work. We've include the
.htaccessfile in the
publicfolder. If rewriting is not working for you, please check that the
mod_rewritemodule (htaccess support) is enabled in the Apache configuration.
Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php [QSA,L] RedirectMatch 307 ^/$ /index.php/
On IIS you have to add some lines your
web.configfile. If rewriting is not working for you, please check that your IIS version have included the
url rewritemodule or download and install them from Microsoft web site.
Please note that the following snippets only covers how to use this router in a project without an existing framework using DefaultMatcher class. If you are using a framework or/and a different
Flight\Routing\Interfaces\RouteMatcherInterfaceclass instance in your project, the implementation varies.
It's not required, but you can set
namespace method parameter's value to eg: 'Demo\\Controllers\\';to prefix all routes with the namespace to your controllers. This will simplify things a bit, as you won't have to specify the namespace for your controllers on each route.
This library uses any PSR-7 implementation, for the purpose of this tutorial, we wil use biurad-http-galaxy library to provide PSR-7 complaint request, stream and response objects to your controllers and middleware
run this in command line if the package has not be added.
composer require biurad/http-galaxy
Flight routing allows you to call any controller action with namespace using
*pattern, also you have have domain on route pattern using
//followed by the host and path, or add a scheme to the pattern.
Create a new file, name it
routes.phpand place it in your library folder or any private folder. This will be the file where you define all the routes for your project.
In your
index.phprequire your newly-created
routes.phpand call the
$collector->handle()method on Publisher
publishmethod, passing an instance of PSR-7
ServerRequestInterface. This will trigger and do the actual routing of the requests to response.
use Flight\Routing\RouteCollector as Router;return static function (Router &$collector): void { $collector->get('phpinfo', '/phpinfo', 'phpinfo'); // Will create a phpinfo route.
// Add more routes here...
};
For dispatching a router, use an instance of
Laminas\HttpHandlerRunner\Emitter\EmitterInterfaceto dispatch the router.
This is an example of a basic
index.phpfile:
use Flight\Routing\{RouteCollector, Router}; use Biurad\Http\Factory\GuzzleHttpPsr7Factory as Psr17Factory; use Laminas\HttpHandlerRunner\Emitter\SapiStreamEmitter;$collector = new RouteCollector();
/* Load external routes file */ (require_once DIR.'/routes.php')($collector);
// Need to have an idea about php before using this dependency, though it easy to use. $psr17Factory = new Psr17Factory();
$router = new Router($psr17Factory, $psr17Factory); $router->addRoute(...$collector->getCollection());
/**
// Start the routing (new SapiStreamEmitter())->emit($router->handle(Psr17Factory::fromGlobalRequest()));
Remember the
routes.phpfile you required in your
index.php? This file be where you place all your custom rules for routing.
NOTE: If your handler return type isn't instance of ResponseInterface, FLight Routing will choose the best content-type for http response. Returning strings can be a bit of conflict for Flight routing, so it fallback is "text/html", a plain text where isn't xml or doesn't have a ... wrapped around contents will return a content-type of text/plain.
The Route class can accept a handler of type
Psr\Http\Server\RequestHandlerInterface, callable,invocable class, or array of [class, method]. Simply pass a class or a binding name instead of a real object if you want it to be constructed on demand.
This library is shipped with annotation and file callable loading, the
Flight\Routing\RouteLoaderclass takes an instance of
Flight\Routing\Interfaces\RouteCollectorInterface. Then use
Flight\Routing\Router::addRouteto load the attached routes from collection using
Flight\Routing\RouteLoader::loadmethod.
use Biurad\Annotations\AnnotationLoader; use Biurad\Http\Factory\GuzzleHttpPsr7Factory as Psr17Factory; use Flight\Routing\Annotation\Listener; use Flight\Routing\RouteCollector; use Flight\Routing\Router; use Spiral\Attributes\AnnotationReader; use Spiral\Attributes\AttributeReader; use Spiral\Attributes\Composite\MergeReader;$loader = new AnnotationLoader(new MergeReader([new AnnotationReader(), new AttributeReader()])); $loader->attachListener(new Listener(new RouteCollector()));
$loader->attach( 'src/Controller', 'src/Bundle/BundleName/Controller', ];
// Need to have an idea about php before using this dependency, though it easy to use. $psr17Factory = new Psr17Factory();
$router = new Router($psr17Factory, $psr17Factory); $router->loadAnnotation($loader);
Below is a very basic example of setting up a route. First parameter is the url which the route should match - next parameter is a
Closureor callback function that will be triggered once the route matches.
$collector->get('home', '/', function() { return 'Hello world'; }); // $collector from routes.php file.
It is possible to pass the
closureas route handler, in this case our function will receive two arguments:
Psr\Http\Message\ServerRequestInterfaceand
Psr\Http\Message\ResponseInterface. Patterned name from path can be added, including from PSR-11 container.
$collector->get( 'hello', '/{name}', function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write("hello world");return $response; }
));
You can catch the request object like this example:
use Biurad\Http\ServerRequest; use Biurad\Http\Response\{EmptyResponse, JsonResponse};$collector->get( 'api_home', '/', function (ServerRequest $request) { return new JsonResponse([ 'method' => $request->getMethod(), 'uri' => $request->getUri(), 'body' => $request->getBody(), 'parsedBody' => $request->getParsedBody(), 'headers' => $request->getHeaders(), 'queryParameters' => $request->getQueryParams(), 'attributes' => $request->getAttributes(), ]); } );
$collector->post( 'api_post', '/blog/posts', function (ServerRequest $request) { $post = new \Demo\Models\Post(); $post->title = $request->getQueryParams()['title']; $post->content = $request->getQueryParams()['content']; $post->save();
return new EmptyResponse(201); }
);
The example below illustrates supported kinds of responses.
use Biurad\Http\Response\{EmptyResponse, HtmlResponse, JsonResponse, TextResponse};$collector ->get( 'html_string_response', '/html/1', function () { return 'This is an HTML response'; } ); $collector ->get( 'html_response', '/html/2', function () { return new HtmlResponse('This is also an HTML response', 200); } ); $collector ->get( 'json_response', '/json', function () { return new JsonResponse(['message' => 'Unauthorized!'], 401); } ); $collector ->get( 'text_response', '/text', function () { return new TextResponse('This is a plain text...'); } ); $collector ->get( 'empty_response', '/empty', function () { return new EmptyResponse(); } );
In case of needing to redirecting user to another URL:
use Biurad\Http\Response\RedirectResponse;$collector ->get('redirect', '/redirect', function () { return new RedirectResponse('https://biurad.com'); });
Here you can see how to declare different routes with different http methods:
head('head', '/', function () { return 'HEAD method'; }); $collector ->get('get', '/', function () { return 'GET method'; }); $collector ->post('post', '/', function () { return 'POST method'; }); $collector ->patch('patch', '/', function () { return 'PATCH method'; }); $collector ->put('put', '/', function () { return 'PUT method'; }); $collector ->options('options', '/', function () { return 'OPTIONS method'; }); $collector ->delete('delete', '/', function () { return 'DELETE method'; });
Sometimes you might need to create a route that accepts multiple HTTP-verbs. If you need to match all HTTP-verbs you can use the
anymethod.
$collector->map('map', ['get', 'post'], '/', function() { // ... });$collector->any('any', 'foo', function() { // ... });
You can use route pattern to specify any number of required and optional parameters, these parameters will later be passed to our route handler via
ServerRequestInterfaceattribute
route.
Use the
{parameter_name:pattern}form to define a route parameter, where pattern is a regexp friendly expression. You can omit pattern and just use
{parameter_name}, in this case the parameter will match
[^\/]+.
You'll properly wondering by know how you parse parameters from your urls. For example, you might want to capture the users id from an url. You can do so by defining route-parameters.
$collector->get('user', '/user/{userId}', function ($userId) { return 'User with id: ' . $userId; });
You may define as many route parameters as required by your route:
$collector->get('posts_comment', '/posts/{postId}/comments/{commentId}', function ($postId, $commentId) { // ... });
Occasionally you may need to specify a route parameter, but make the presence of that route parameter optional. Use
[]to make a part of route (including the parameters) optional, for example:
// Optional parameter $collector->get('optional_with_slash', '/user[/{name}]', function ($name = null) { return $name; }); //or $collector->get('optional', '/user[/{name}]', function ($name) { return $name; });
// Optional parameter with default value $collector->get('optional_without_slash', '/user/[{name}]', function ($name = 'Simon') { return $name; }); //or $collector->get('optional_with_default', '/user/[{name=}]', function ($name) { return $name; });
Obviously, if a parameter is inside an optional sequence, it's optional too and defaults to
null. Sequence should define it's surroundings, in this case a slash which must follow a parameter, if set. The technique may be used for example for optional language subdomains:
$collector->get('lang', '//[{lang=}.]example.com/hello', ...);
Sequences may be freely nested and combined:
$collector->get('lang_with_default', '[{lang:[a-z]{2}}[-{sublang}]/]{name}[/page-{page=<0>}]', ...);// Accepted URLs: // /cs/hello // /en-us/hello // /hello // /hello/page-12 // /ru/hello/page-12
Note: Route parameters are always encased within {} braces and should consist of alphabetic characters. Route parameters may not contain a - character. Use an underscore (_) instead.
You may constrain the format of your route parameters using the where method on a route instance. The where method accepts the name of the parameter and a regular expression defining how the parameter should be constrained:
$collector->get('pattern_string', '/user/{name}', function ($name) { // })->addPattern('name', '[A-Za-z]+');$collector->get('pattern_int', '/user/{id}', function (int $id) { // })-addPattern('id', '[0-9]+');
$collector->get('patterned', '/user/{id}/{name}', function (int $id, string $name) { // })->setPatterns(['id' => '[0-9]+', 'name' => '[a-z]+']);
$collector->get('pattern_in_path', '/user/{id:[0-9]+}/{name:[a-z]+}', function (int $id, string $name) { // });
Named routes allow the convenient generation of URLs or redirects for specific routes. It is mandatory to specify a name for a route by chaining the name onto the first argument of route definition:
$collector->get('profile', '/user/profile', function () { // Your code here };
You can also specify names for grouping route with a prefixed name:
$collector->group(function ($group) { $group->get('profile', '/user/profile', '[email protected]'); })->setName('user.'); // Will produce "user.profile"
URL generator tries to keep the URL as short as possible (while unique), so what can be omitted is not used. The behavior of generating urls from route depends on the respective parameters sequence given.
Once you have assigned a name to a given route, you may use the route's name, its parameters and maybe add query, when generating URLs:
// Generating URLs... $url = $collector->generateUri('profile');
If the named route defines parameters, you may pass the parameters as the second argument to the
urlfunction. The given parameters will automatically be inserted into the URL in their correct positions:
$collector->get('profile', '/user/{id}/profile', function ($id) { // });$url = $collector->generateUri('profile', ['id' => 1]); // will produce "user/1/profile"
Route groups allow you to share route attributes, such as middlewares, namespace, domain, name, prefix, patterns, or defaults, across a large number of routes without needing to define those attributes on each individual route. Shared attributes are specified in an array format as the first parameter to the
$collector->groupmethod.
use Flight\Routing\Interfaces\RouteCollectorInterface;$collector->group( function (RouteCollectorInterface $route) { // Define your routes using $route... } );
Router supports middleware, you can use it for different purposes like authentication, authorization, throttles and so forth. Middleware run before controllers and it can check and manipulate http requests. To associate route specific middleware use
addMiddleware, you can access route parameters via
argumentsattribute of the request object:
Here you can see the request lifecycle considering some middleware:
Input --[Request]↦ Router ↦ Middleware 1 ↦ ... ↦ Middleware N ↦ Controller ↧ Output ↤[Response]- Router ↤ Middleware 1 ↤ ... ↤ Middleware N ↤ [Response]
We using using [laminas-stratigility] to allow better and saver middleware usage.
run this in command line if the package has not be added.
composer require laminas/laminas-stratigility
To declare a middleware, you must implements Middleware
Psr\Http\Server\MiddlewareInterfaceinterface.
Middleware must have a
process()method that catches http request and a closure (which runs the next middleware or the controller) and it returns a response at the end. Middleware can break the lifecycle and return a response itself or it can run the
$handlerimplementing
Psr\Http\Server\RequestHandlerInterfaceto continue lifecycle.
For example see the following snippet. In this snippet, we will demonstrate how a middleware works:
use Demo\Middleware\ParamWatcher; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface;$collector->get( 'watch', '/{param}', function (ServerRequestInterface $request, ResponseInterface $response) { return $request->getAttribute('arguments'); } )) ->addMiddleware(ParamWatcher::class);
where
ParamWatcheris:
namespace Demo\Middleware;use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Biurad\Http\Exceptions\ClientException\UnauthorizedException;
class ParamWatcher implements MiddlewareInterface { public function process(Request $request, RequestHandlerInterface $handler): Response { if ($request->getAttribute('arguments')['param'] === 'forbidden') { throw new UnauthorizedException(); }
return $handler->handle($request); }
}
This route will trigger Unauthorized exception on
/forbidden.
You can add as many middlewares as you want. Middlewares can be implemented using closures but it doesn’t make sense to do so!
Flight Routing increases SEO (search engine optimization) as it prevents multiple URLs to link to different content (without a proper redirect). If more than one addresses link to the same target, the router choices the first (makes it canonical), while the other routes are never reached. Thanks to that your page won't have duplicities on search engines and their rank won't be split.
Router will match all routes in the order they were registered. Make sure to avoid situations where previous route matches the conditions of the following routes.
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface;$collector->get( 'reachable', '/{param}', function (ServerRequestInterface $request, ResponseInterface $response) { return $request->getAttribute('arguments'); } ))
// this route will never trigger $collector->get( 'non_reachable', '/hello', function (ServerRequestInterface $request, ResponseInterface $response) { return $request->getAttribute('arguments'); } ))
Route groups may also be used to handle sub-domain routing. The sub-domain may be specified using the
domainkey on the group attribute array:
use Flight\Routing\Interfaces\RouteCollectorInterface;// Domain $collector->get('domain', '/', '[email protected]')->setDomain('domain.com');
// Subdomain $collector->get('sub_domain', '/', 'Controller:method')->setDomain('server2.domain.com');
// Subdomain regex pattern $collector->get('account_domain', '/', ['Controller', 'method'])->setDomain('{accounts:.*}.domain.com');
$collector->group(function (RouteCollectorInterface $route) { $route->get('/user/{id}', function ($id) { // }); })->setDomain('account.myapp.com');
All of
Flight\Routing\Routehas a restful implementation, which specifies the method selection behavior. Add a
__restfulprefix to a route name or use
Flight\Routing\RouteCollector::resourcemethod to automatically prefix all the methods in
Flight\Routing\Interfaces\RouteCollectorInterface::HTTP_METHODS_STANDARDwith HTTP verb.
For example, we can use the following controller:
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\Router::addRoute:
use Demo\UserController;$router->addRoute(new Route( 'user__restful', '/user/{id:\d+}', [UserController::class, 'user'] ));
Add route using
Flight\Routing\RouteCollector::resource:
use Demo\UserController;$collector->resource('user', '/user/{id:\d+}', UserController::class);
Invoking
/user/1with different HTTP methods will call different controller methods. Note, you still need to specify the action name.
If these offered routes do not fit your needs, you may create your own router matcher and add it to your
router collector. Router is nothing more than an implementation of RouteMatcherInterface with its four methods:
use Flight\Routing\Interfaces\RouteMatcherInterface; use Flight\Routing\Interfaces\RouteInterface;class MyRouteMatcher implements RouteMatcherInterface { /** * {@inheritdoc} */ public function compileRoute(RouteInterface $route): RouteMatcherInterface { // ... compile the route for matching }
/** * {@inheritdoc} */ public function buildPath(RouteInterface $route, array $substitutions): string { // ... generate a named route path using $substitutions } /** * {@inheritdoc} */ public function getRegex(bool $domain = false): string { // ... the regex generated using this class "compileRoute" method } /** * {@inheritdoc} */ public function getVariables(): array { // ... the variables generated using this class "compileRoute" method }
}
For in-depth documentation before using this library.. Full documentation on advanced usage, configuration, and customization can be found at docs.biurad.com.
Information on how to upgrade to newer versions of this library can be found in the UPGRADE.
SemVer is followed closely. Minor and patch releases should not introduce breaking changes to the codebase; See CHANGELOG for more information on what has changed recently.
Any classes or methods marked
@internalare not intended for use outside of this library and are subject to breaking changes at any time, so please avoid using them.
When a new major version is released (
1.0,
2.0, etc), the previous one (
0.19.x) will receive bug fixes for at least 3 months and security updates for 6 months after that new release comes out.
(This policy may change in the future and exceptions may be made on a case-by-case basis.)
Professional support, including notification of new releases and security updates, is available at Biurad Commits.
To report a security vulnerability, please use the Biurad Security. We will coordinate the fix and eventually commit the solution in this project.
Contributions to this library are welcome, especially ones that:
Please see CONTRIBUTING for additional details.
$ composer test
This will tests biurad/php-cache will run against PHP 7.2 version or higher.
This code is partly a reference implementation of Sunrise Http Router which is written, maintained and copyrighted by Anatoly Fenric. This project new features starting from version
1.0was referenced from Sunrise Http Router
Are you interested in sponsoring development of this project? Reach out and support us on Patreon or see https://biurad.com/sponsor for a list of ways to contribute.
divineniiquaye/flight-routing is licensed under the BSD-3 license. See the
LICENSEfile for more details.
This project is primarily maintained by Divine Niiquaye Ibok. Members of the Biurad Lap Leadership Team may occasionally assist with some of these duties.
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us an email or message mentioning this library. We publish all received request's at https://patreons.biurad.com.
Check out the other cool things people are doing with
divineniiquaye/flight-routing: https://packagist.org/packages/divineniiquaye/flight-routing/dependents