phossa2/route [ABANDONED]
PLEASE USE phoole/route library instead, (*1)
, (*2)
phossa2/route is a fast, full-fledged and feature-rich application
level routing library for PHP., (*3)
It requires PHP 5.4, supports PHP 7.0+ and HHVM. It is compliant with PSR-1,
PSR-2, PSR-3, PSR-4, and the proposed PSR-5., (*4)
Why another routing library ?
Installation
Install via the composer
utility., (*14)
composer require "phossa2/route"
or add the following lines to your composer.json
, (*15)
{
"require": {
"phossa2/route": "2.*"
}
}
Usage
Inject route definitions (pattern, handler, default values etc.) into the
dispatcher and then call either match()
or dispatch()
., (*16)
use Phossa2\Route\Route;
use Phossa2\Route\Dispatcher;
// dispatcher with default collector & resolver
$dispatcher = (new Dispatcher())
->addGet(
'/blog/{action:xd}[/{year:d}[/{month:d}[/{date:d}]]]',
function($result) {
$params = $result->getParameters();
echo "action is " . $params['action'];
}
)->addPost(
'/blog/post',
'handler2'
)->addRoute(new Route(
'GET,HEAD',
'/blog/read[/{id:d}]',
'handler3',
['id' => '1'] // default values
));
// diaptcher (match & execute controller action)
$dispatcher->dispatch('GET', '/blog/list/2016/05/01');
Or load routes from an array,, (*17)
$routes = [
'/user/{action:xd}/{id:d}' => [
'GET,POST', // methods,
function ($result) {
$params = $result->getParameters();
echo "user id is " . $params['id'];
}, // handler,
['id' => 1] // default values
],
// ...
];
$dispatcher = (new Dispatcher())->loadRoutes($routes);
$dispatcher->dispatch('GET', '/user/view/123456');
Route syntax
-
{Named} parameters, (*18)
A route pattern syntax is used where {foo}
specifies a named parameter or
a placeholder with name foo
and default regex pattern [^/]++
. In order to
match more specific types, you may specify a custom regex pattern like
{foo:[0-9]+}
., (*19)
// with 'action' & 'id' two named params
$dispatcher->addGet('/user/{action:[^0-9/][^/]*}/{id:[0-9]+}', 'handler1');
Predefined shortcuts can be used for placeholders as follows,, (*20)
':d}' => ':[0-9]++}', // digit only
':l}' => ':[a-z]++}', // lower case
':u}' => ':[A-Z]++}', // upper case
':a}' => ':[0-9a-zA-Z]++}', // alphanumeric
':c}' => ':[0-9a-zA-Z+_\-\.]++}', // common chars
':nd}' => ':[^0-9/]++}', // not digits
':xd}' => ':[^0-9/][^/]*+}', // no leading digits
The previous pattern can be rewritten into,, (*21)
// with 'action' & 'id' two named params
$dispatcher->addGet('/user/{action:xd}/{id:d}', 'handler1');
-
[Optional] segments, (*22)
Optional segments in the route pattern can be specified with []
as follows,, (*23)
// $action, $year/$month/$date are all optional
$pattern = '/blog[/{action:xd}][/{year:d}[/{month:d}[/{date:d}]]]';
where optional segments can be NESTED. Unlike other libraries, optional
segments are not limited to the end of the pattern, as long as it is a valid
pattern like the [/{action:xd}]
in the example., (*24)
-
Syntax limitations, (*25)
- Parameter name MUST start with a character
Since {2}
has special meanings in regex. Parameter name MUST start with
a character. And the use of {}
inside/outside placeholders may cause
confusion, thus is not recommended., (*26)
-
[]
outside placeholder means OPTIONAL segment only
[]
can not be used outside placeholders as part of a regex pattern, IF
YOU DO NEED to use them as part of the regex pattern, please include them
INSIDE a placeholder., (*27)
- Use of capturing groups
()
inside placeholders is not allowed
Capturing groups ()
can not be used inside placeholders. For example
{user:(root|phossa)}
is not valid. Instead, you can use either use
{user:root|phossa}
or {user:(?:root|phossa)}
., (*28)
-
Default Values, (*29)
Default values can be added to named parameters at the end in the form of
{action:xd=list}
. Default values have to be alphanumeric chars. For example,, (*30)
// $action, $year/$month/$date are all optional
$pattern = '/blog[/{action:xd=list}][/{year:d=2016}[/{month:d=01}[/{date:d=01}]]]';
$dispatcher->addGet($pattern, function($result) {
$params = $result->getParameters();
echo $params['year'];
})->dispatch('GET', '/blog');
Routes
-
Defining routes with dispatcher, (*31)
You may define routes with dispatcher. But, it is actually defining routes
with the first route collector in the dispatcher., (*32)
// a new route collector will be added automatically if not yet
$dispatcher = (new Dispatcher())->addPost('/blog/post', 'handler2');
addGet()
and addPost()
are wrappers of addRoute(RouteInterface)
., (*33)
-
Multiple routing collectors, (*34)
Routes can be grouped into different collections by using multiple collectors., (*35)
use Phossa2\Route\Collector\Collector;
// '/user' related
$collector_user = (new Collector())
->addGet('/user/list/{id:d}', 'handler1')
->addGet('/user/view/{id:d}', 'handler2')
->addPost('/user/new', 'handler3');
// '/blog' related
$collector_blog = (new Collector())
->addGet('/blog/list/{user_id:d}', 'handler4')
->addGet('/blog/read/{blog_id:d}', 'handler5');
$dispatcher->addCollector($collector_user)
->addCollector($collector_blog);
-
Path prefix matching, (*36)
Collectors may set a path prefix using setPathPrefix()
to indicate the exact
URI path prefix is handling. Any non-matching prefix found will skip the
collector entirely., (*37)
// '/user/' prefix
$collector_user = (new Collector())
->setPathPrefix('/user/')
->addGet('/user/list/{id:d}', 'handler1')
->addGet('/user/view/{id:d}', 'handler2')
->addPost('/user/new', 'handler3');
-
Same route pattern, (*38)
User can define same route pattern with different http methods., (*39)
$dispatcher
->addGet('/user/{$id}', 'handler1')
->addPost('/user/{$id}', 'handler2');
Dispatching
-
Dispatch with dispatcher's dispatch()
, (*40)
$dispatcher->dispatch('GET', '/user/view/123');
-
Match instead of dispatching, (*41)
Instead of executing handler by default in dispatch()
, more control by
user if using the match()
method, (*42)
if ($dispatcher->match('GET', '/user/view/1234')) {
$result = $dispatcher->getResult();
switch($result->getStatus()) {
case 200:
// ...
break;
case 404:
// ...
break;
default:
// ...
break;
}
} else {
// no match found
// ...
}
Handlers
-
Route handler, (*43)
Route is defined with a handler for status 200 OK
only., (*44)
use Phossa2\Route\Route;
use Phossa2\Route\Status;
$route = new Route(
'GET',
'/user/{action:xd}/{id:d}',
function($result) { // handler for Status::OK
// ...
}
);
-
Default handlers, (*45)
Dispatcher and collectors can have multiple handlers corresponding to
different result status., (*46)
If the result has no handler set (for example, no match found), then the
collector's handler(same status code) will be retrieved. If still no luck,
the dispatcher's handler (same status code) will be used if defined., (*47)
Dispatcher-level handlers,, (*48)
use Phossa2\Route\Status;
$dispatcher->addHandler(
function($result) {
echo "method " . $result->getMethod() . " not allowed";
},
Status::METHOD_NOT_ALLOWED
);
Collector-level handlers,, (*49)
$collector->addHandler(
function($result) {
// ...
},
Status::MOVED_PERMANENTLY
);
When addHandler()
with status set to 0
will cause this handler be the
default handler for other status., (*50)
use Phossa2\Route\Status;
$dispatcher->addHandler(
function($result) {
echo "no other handler found";
},
0 // <-- match all other status
);
-
Handler resolving, (*51)
Most of the time, matching route will return a handler like
[ 'ControllerName', 'actionName' ]
. Handler resolver can be used to
resolving this pseudo handler into a real callable., (*52)
use Phossa2\Route\Collector\Collector;
use Phossa2\Route\Resolver\ResolverSimple;
// dispatcher with default resolver
$dispatcher = new Route\Dispatcher(
new Collector(),
new ResolverSimple() // the default resolver anyway
);
Users may write their own handler resolver by implementing
Phossa2\Route\Interfaces\ResolverInterface
., (*53)
Extensions
Extensions are callables dealing with the matching result or other tasks before
or after certain dispatching stages., (*54)
Extensions can be added to Dispatcher
, Collector
or even Route
., (*55)
-
Use of extensions, (*56)
Extensions MUST return a boolean value to indicate wether to proceed with
the dispatching process or not. FALSE
means stop and returns to top level., (*57)
Extensions can be either a Phossa2\Event\EventableExtensionAbstract
and added
with addExtension()
or addExt()
, or a callable with signature of
callableName(Phossa2\Event\EventInterface $event): bool
which can be added
as extension via addExt(callable, eventName, priority)
., (*58)
use Phossa2\Route\Status;
use Phossa2\Route\Dispatcher;
use Phossa2\Route\Extensions\RedirectToHttps;
// create dispatcher
$dispatcher = new Dispatcher();
// direct any HTTP request to HTTPS port before any routing
$dispatcher
->addExtension(new RedirectToHttps())
->addHandler(function() {
echo "redirect to https";
}, Status::MOVED_PERMANENTLY)
->dispatch('GET', '/user/view/123');
Force authentication for any '/user/' prefixed URL,, (*59)
use Phossa2\Route\Status;
use Phossa2\Route\Dispatcher;
use Phossa2\Route\Extensions\UserAuth;
$dispatcher = new Dispatcher();
$dispatcher
// add handler for unauthorized routing
->addHandler(
function() {
echo "need auth";
}, Status::UNAUTHORIZED)
// add a route
->addGet('/user/view/{id:d}', function() {
echo "AUTHED!";
})
// add extension to force auth routes under '/user/'
->addExt(function($event) {
$result = $event->getParam('result');
$path = $result->getPath();
if (!isset($_SESSION['authed']) && '/user/' === substr($path, 0, 6)) {
$result->setStatus(Status::UNAUTHORIZED);
return false;
}
return true;
}, Dispatcher::EVENT_BEFORE_MATCH);
// try a not authed route
$dispatcher->dispatch('GET', '/user/view/123');
// try a authed route
$_SESSION['authed'] = 1;
$dispatcher->dispatch('GET', '/user/view/123');
-
Examples of extension, (*60)
Validation of a parameter value on a route,, (*61)
use Phossa2\Route\Status;
use Phossa2\Route\Dispatcher;
use Phossa2\Route\Extensions\IdValidation;
$dispatcher = new Dispatcher();
// add extension to a route
$route = (new Route('GET', '/user/{id:d}', null))
->addExtension(new IdValidation());
// will fail
$dispatcher->addRoute($route)->dispatch('GET', '/user/1000');
-
Extension events, (*62)
Three types of events, dispatcher level, collector level and route level.
List of all events in the order of execution., (*63)
-
Dispatcher::EVENT_BEFORE_MATCH
before matching starts, (*64)
-
Collector::EVENT_BEFORE_MATCH
before matching in a collector, (*65)
-
Collector::EVENT_AFTER_MATCH
after a successful match in the collector, (*66)
-
Dispatcher::EVENT_AFTER_MATCH
after a successful match at dispatcher level, (*67)
-
Dispatcher::EVENT_BEFORE_DISPATCH
after a sucessful match, before
dispatching to any handler, (*68)
-
Route::EVENT_BEFORE_HANDLER
before executing handler(route's or
collector's) for this route, (*69)
-
Route::EVENT_AFTER_HANDLER
after handler successfully executed, (*70)
-
Dispatcher::EVENT_AFTER_DISPATCH
back to dispatcher level, after handler
executed successfully, (*71)
-
Dispatcher::EVENT_BEFORE_HANDLER
match failed or no handler found for the
matching route, before execute dispatcher's default handler, (*72)
-
Dispatcher::EVENT_AFTER_HANDLER
after dispatcher's default handler
executed, (*73)
Debugging
Sometimes, you need to know what went wrong., (*74)
$dispatcher->enableDebug()->setDebugger($logger);
Where $logger
is a PSR-3 compatible logger implmenting the interface
Psr\Log\LoggerInterface
. The dispatcher will send logs of dispatching process
to the logger., (*75)
Routing strategies
There are a couple of URL based routing strategies supported in this library.
Different strategy collectors can be combined together into one dispatcher., (*76)
-
Parameter Pairs Routing (PPR), (*77)
Using parameter and value pairs for routing,, (*78)
http://servername/path/index.php/controller/action/id/1/name/nick
Parameters order can be arbitary, but have to appear in pairs. Advantage of
this scheme is fast and web crawler friendly. If URL rewriting is used, the
above can be written into the following,, (*79)
http://servername/path/controller/action/id/1/name/nick
Instead of using '/' as the parameter seperator, any URL valid characters
except for the '?' and '&' can be used as a seperator., (*80)
http://servername/path/controller-action-id-1-name-nick
This strategy is implemented in Phossa2\Route\Collector\CollectorPPR
class., (*81)
-
Query Parameter Routing (QPR), (*82)
The routing info is directly embedded in the URL query. The advantage of this
scheme is fast and clear., (*83)
http://servername/path/?r=controller-action-id-1-name-nick
This strategy is implemented in Phossa2\Route\Collector\CollectorQPR
class., (*84)
-
Regular Expression Routing (RER), (*85)
Regular expression based routing is the default routing strategy for this
library and implemented in Phossa2\Route\Collector\Collector
class., (*86)
// created with default RER collector
$dispatcher = (new Dispatcher())
->addCollector(new Collector()) // regex based routing first
->addCollector(new CollectorQPR()); // support for legacy QPR
Regex matching algorithms
Different regex matching algorithms can be used with the RER collector., (*87)
-
FastRoute algorithm, (*88)
This Group Count Based algorithm is implemented in
Phossa2\Route\Parser\ParserGcb
class and explained in detail in this article
"Fast request routing using regular expressions"., (*89)
phossa-route uses this algorithm by default., (*90)
-
Standard algorithm, (*91)
This algorithm is developed by phossa2/route and a little bit slower than the
fastRoute GCB algorithm. It is implemented in Phossa2\Route\Parser\ParserStd
class., (*92)
Use this standard algorithm,, (*93)
use Phossa2\Route\Dispatcher;
use Phossa2\Route\Parser\ParserStd;
use Phossa2\Route\Collector\Collector;
// use standard algorithm
$dispatcher = new Dispatcher(new Collector(new ParserStd));
-
Comments on routing algorithms, (*94)
- It does NOT matter that much as you may think.
If you are using routing library in your application, different algorithms
may differ only 0.1 - 0.2ms for a single request, which seems meaningless
for an application unless you are using it as a standalone router., (*95)
- If you DO care about routing speed
Use different routing strategy like Parameter Pairs Routing (PPR)
which is much faster than the regex based routing. Also by
carefully design your routes, you may achieve better results even if you
are using a slower algorithm., (*96)
Change log
Please see CHANGELOG from more information., (*97)
Testing
$ composer test
Contributing
Please see CONTRIBUTE for more information., (*98)
Dependencies
-
PHP >= 5.4.0, (*99)
-
phossa2/event >= 2.1.5, (*100)
-
phossa2/shared >= 2.0.27, (*101)
License
MIT License, (*102)
Appendix
-
Performance, (*103)
This benchmark matches the last route and unknown route. It generates a
randomly prefixed and suffixed route in an attempt to thwart any optimization.
1,000 routes each with 8 arguments., (*104)
This benchmark consists of 14 tests. Each test is executed 1,000 times, the
results pruned, and then averaged. Values that fall outside of 3 standard
deviations of the mean are discarded., (*105)
"Parameter Pairs Routing (PPR)" is fastest and used as baseline., (*106)
Test Name |
Results |
Time |
+ Interval |
Change |
Phossa PPR - unknown route (1000 routes) |
998 |
0.0000724551 |
+0.0000000000 |
baseline |
Phossa PPR - last route (1000 routes) |
993 |
0.0000925307 |
+0.0000200755 |
28% slower |
Symfony2 Dumped - unknown route (1000 routes) |
998 |
0.0004353616 |
+0.0003629065 |
501% slower |
Phroute - last route (1000 routes) |
999 |
0.0006205601 |
+0.0005481050 |
756% slower |
Phossa - unknown route (1000 routes) |
998 |
0.0006903790 |
+0.0006179239 |
853% slower |
FastRoute - unknown route (1000 routes) |
1,000 |
0.0006911943 |
+0.0006187392 |
854% slower |
FastRoute - last route (1000 routes) |
999 |
0.0006962751 |
+0.0006238200 |
861% slower |
Phroute - unknown route (1000 routes) |
998 |
0.0007134676 |
+0.0006410125 |
885% slower |
Symfony2 Dumped - last route (1000 routes) |
993 |
0.0008066097 |
+0.0007341545 |
1013% slower |
Phossa - last route (1000 routes) |
998 |
0.0009104498 |
+0.0008379947 |
1157% slower |
Symfony2 - unknown route (1000 routes) |
989 |
0.0023998006 |
+0.0023273455 |
3212% slower |
Symfony2 - last route (1000 routes) |
999 |
0.0025880890 |
+0.0025156339 |
3472% slower |
Aura v2 - last route (1000 routes) |
981 |
0.0966411463 |
+0.0965686912 |
133281% slower |
Aura v2 - unknown route (1000 routes) |
992 |
0.1070026719 |
+0.1069302168 |
147581% slower |
This benchmark tests how quickly each router can match the first route. 1,000
routes each with 8 arguments., (*107)
This benchmark consists of 7 tests. Each test is executed 1,000 times, the
results pruned, and then averaged. Values that fall outside of 3 standard
deviations of the mean are discarded., (*108)
Note Both FastRoute and Phroute implement a static route table, so
they are fast at the first route matching (which is a static route), (*109)
Test Name |
Results |
Time |
+ Interval |
Change |
FastRoute - first route |
999 |
0.0000403543 |
+0.0000000000 |
baseline |
Phroute - first route |
998 |
0.0000405911 |
+0.0000002368 |
1% slower |
Symfony2 Dumped - first route |
999 |
0.0000590617 |
+0.0000187074 |
46% slower |
Phossa PPR - first route |
977 |
0.0000678727 |
+0.0000275184 |
68% slower |
Phossa - first route |
999 |
0.0000898475 |
+0.0000494932 |
123% slower |
Symfony2 - first route |
998 |
0.0003983802 |
+0.0003580259 |
887% slower |
Aura v2 - first route |
986 |
0.0004391784 |
+0.0003988241 |
988% slower |
-
URL rewrite, (*110)
Setup URL rewriting to do routing with index.php
, (*111)
- Apache
.htaccess
with mod_rewrite
engine is on
DirectorySlash Off
Options -MultiViews
DirectoryIndex index.php
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^ index.php [QSA,L]
and in your httpd.conf
file to enable using of .htaccess
, (*112)
<VirtualHost *:80>
ServerAdmin me@mysite.com
DocumentRoot "/path/www.mysite.com/public"
ServerName mysite.com
ServerAlias www.mysite.com
<Directory "/path/www.mysite.com/public">
Options -Indexes +FollowSymLinks +Includes
AllowOverride All
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
- Nginx configration in
nginx.conf
server {
listen 80;
server_name www.mysite.com mysite.com;
root /path/www.mysite.com/public;
try_files $uri $uri/ /index.php$is_args$args;
location /index.php {
fastcgi_connect_timeout 3s;
fastcgi_read_timeout 10s;
include fastcgi.conf;
fastcgi_pass 127.0.0.1:9000;
}
}
-
Routing issues, (*113)
Base on the request informations, such as request device, source ip, request
method etc., service provider may direct request to different hosts, servers,
app modules or handlers., (*114)
Common case, such as routing based on request's source ip, routes the
request to a NEAREST server, this is common in content distribution
network (CDN), and is done at network level., (*115)
For performance reason, some of the simple routing can be done at web
server level, such as using apache or ngix configs to do simple routing., (*116)
For example, if your server goes down for maintenance, you may replace
the .htaccess
file as follows,, (*117)
DirectorySlash Off
Options -MultiViews
DirectoryIndex maintenance.php
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^ maintenance.php [QSA,L]
It solves much more complicated issues, and much more flexible., (*118)
Usually, routing is done at a single point index.php
. All the requests
are configured to be handled by this script first and routed to different
routines., (*119)