2017 © Pedro PelĆ”ez
 

library phossa-route

A fast and full-fledged routing libraray for PHP

image

phossa/phossa-route

A fast and full-fledged routing libraray for PHP

  • Monday, June 13, 2016
  • by phossa
  • Repository
  • 1 Watchers
  • 2 Stars
  • 11 Installations
  • PHP
  • 1 Dependents
  • 0 Suggesters
  • 0 Forks
  • 0 Open issues
  • 4 Versions
  • 0 % Grown

The README.md

phossa-route [ABANDONED]

Build Status Latest Stable Version License, (*1)

See new lib at phoole/route, (*2)

Introduction

phossa-route is a fast, full-fledged and feature-rich application level routing library for PHP. It dispatches requests base on URLs, HTTP headers, session informations etc., (*3)

It requires PHP 5.4 and supports PHP 7.0+, HHVM. It is compliant with PSR-1, PSR-2, PSR-4., (*4)

Why another routing library ?

Getting started

  • Installation, (*15)

    Install via the composer utility., (*16)

    composer require "phossa/phossa-route=1.*"
    

    or add the following lines to your composer.json, (*17)

    {
      "require": {
        "phossa/phossa-route": "^1.0.0"
      }
    }
    
  • Simple usage, (*18)

    use Phossa\Route\Dispatcher;
    
    // dispatcher with default collector & resolver
    $dispatcher = (new Dispatcher())
      ->addGet(
          '/blog/{action:xd}[/{year:d}[/{month:d}[/{date:d}]]]',
          function($result) {
              echo "action is " . $result->getParameter('action');
          })
      ->addPost('/blog/post', 'handler2')
      ->addRoute(new Route\Route(
          'GET,HEAD', // multiple methods
          '/blog/read[/{id:d}]',
          'handler3',
          ['id' => '1'])); // default $id value
    
    // route base on info provided by server
    $dispatcher->dispatch();
    
  • Load routes from file, (*19)

    use Phossa\Route\Dispatcher;
    
    /*
    * routes.php :
    * return [
    *     '/user/phossa'  => 'handler1',
    *     '/user/{action:xd}/{id:d}'   => [['controller', 'action'], 'GET,POST'],
    *     '/user/view[/{id:d}]' => ['handler2', 'GET', ['id' => 23]]
    * ];
    */
    $dispatcher = (new Dispatcher())->loadRoute('./routes.php');
    

Route syntax

NOTE: Support for other route library syntax is on the release 1.1.x list. User may pick his/her favorite route syntax with phossa-route :)., (*20)

  • {Named} parameters, (*21)

    A route pattern syntax is used where {foo} specifies a named parameter or a placeholder with name foo and default pattern [^/]++. In order to match more specific types, you may specify a custom regex pattern like {foo:[0-9]+}., (*22)

    // 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,, (*23)

    ':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,, (*24)

    // with 'action' & 'id' two named params
    $dispatcher->addGet('/user/{action:xd}/{id:d}', 'handler1');
    
  • [Optional] segments, (*25)

    Optional segments in the route pattern can be specified with [] as follows,, (*26)

    // $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., (*27)

  • Syntax limitations, (*28)

    • 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., (*29)

    • [] 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., (*30)

    • 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)}., (*31)

  • Default Values, (*32)

    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,, (*33)

    // $action, $year/$month/$date are all optional
    $pattern = '/blog[/{action:xd=list}][/{year:d=2016}[/{month:d=01}[/{date:d=01}]]]';
    

Routes

  • Defining routes with dispatcher, (*34)

    You may define routes with dispatcher, but it is actually defining routes with the first collector (route collection) in the dispatcher., (*35)

    $dispatcher = (new Dispatcher())->addPost('/blog/post', 'handler2');
    

    addGet() and addPost() are wrappers of addRoute(RouteInterface)., (*36)

  • Multiple routing collectors, (*37)

    Routes can be grouped into different collections by using multiple collectors., (*38)

    // '/user' related
    $collector_user = (new Route\Collector\Collector())
      ->addGet('/user/list/{id:d}', 'handler1')
      ->addGet('/user/view/{id:d}', 'handler2')
      ->addPost('/user/new', 'handler3');
    
    // '/blog' related
    $collector_blog = (new Route\Collector\Collector())
      ->addGet('/blog/list/{user_id:d}', 'handler4')
      ->addGet('/blog/read/{blog_id:d}', 'handler5');
    
    $dispatcher->addCollector($collector_user)
             ->addCollector($collector_blog);
    
  • Same route pattern, (*39)

    User can define same route pattern with different http methods., (*40)

    $dispatcher
      ->addGet('/user/{$id}', 'handler1')
      ->addPost('/user/{$id}', 'handler2');
    

    But can not define same route pattern same method with different filters. The possible solution is dealing logic in handler1 or add extensions to the route., (*41)

Dispatching

  • Dispatch with dispatcher's dispatch(), (*42)

    In the script index.php, the dispatch() is normally the last line., (*43)

    // index.php
    // ...
    
    // dispatch base on server request info
    $dispatcher->dispatch();
    

    dispatch() takes one optional argument Phossa\Route\Context\ResultInterface. When none provided, it will collect informations from super globals like $_SERVER and $_REQUEST and dispatches to the right routine or callable base on route definition., (*44)

  • Dispatch an URL, (*45)

    User may dispatch an URL,, (*46)

    $dispatcher->dispatchUrl('GET', '/error404');
    
  • Match instead of dispatching, (*47)

    Instead of executing handler by default in dispatch(), more control by user if using the match() method, (*48)

    // use info from $_SERVER etc.
    if ($dispatcher->match()) {
      $result = $dispatcher->getResult();
      switch($result->getStatus()) {
          case 200:
            // ...
            break;
          case 404:
            // ...
            break;
          default:
            // ...
            break;
      }
    } else {
      // no match found
      // ...
    }
    

    matchUrl() is also provided., (*49)

Handlers

  • Multiple handlers, (*50)

    Route is defined with one handler for status 200 OK. Multiple handlers are supported for other result status., (*51)

    use Phossa\Route\Route;
    use Phossa\Route\Status;
    
    $route = (new Route('GET', '/user/{action:xd}/{id:d}',
      function($result) { // handler for Status::OK
          $user_id = $result->getParameter('id');
          // ...
      })->addHandler(Status::METHOD_NOT_ALLOWED, 'handler1'); // extra handler
    

    Handler handler1 will be executed if route is matched but method is not valid., (*52)

  • Default handlers, (*53)

    Dispatcher and collectors can have multiple handlers corresponding to different status. If the result has no handler set, 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., (*54)

    Dispatcher-level handlers,, (*55)

    use Phossa\Route\Status;
    
    $dispatcher->addHandler(
      Status::SERVICE_UNAVAILABLE,
      function($result) {
          // ...
      }
    );
    

    Collector-level handlers,, (*56)

    $collector->addHandler(
      Status::MOVED_PERMANENTLY,
      function($result) {
          // ...
      }
    );
    
  • Handler resolving, (*57)

    Most of the time, routes returns a handler like [ 'className', 'method' ]. Handler resolver can be used to resolving this pseudo handler into a real callable., (*58)

    use Phossa\Route;
    
    // dispatcher with default resolver
    $dispatcher = new Route\Dispatcher(
      new Route\Collector\Collector(),
      new Route\Handler\ResolverAbstract()
    );
    

    Users may write their own handler resolver by extending Phossa\Route\Handler\ResolverAbstract class., (*59)

Extensions

Extensions are executables dealing with the matching result or other tasks before or after certain dispatching stages., (*60)

  • Use of extensions, (*61)

    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, the dispatcher level., (*62)

    use Phossa\Route\Dispatcher
    use Phossa\Route\Extensions\RedirectToHttpsExtension;
    
    // create dispatcher
    $dispatcher = new Dispatcher();
    
    // direct any HTTP request to HTTPS port before any routing
    dispatcher->addExtension(new RedirectToHttpsExtension());
    

    Force authentication for any '/user' prefixed URL,, (*63)

    $dispatcher->addExtension(
      function($result) {
          $pattern = $result->getRequest()->getPattern();
          if ('/user' == substr($pattern, 0, 5) && !isset($_SESSION['auth'])) {
              $result->setStatus(Status::UNAUTHORIZED);
              return false; // return to dispatcher level
          }
          return true; // authed or not in /user
      },
      Dispatcher::BEFORE_MATCH // run this extension before matching
    );
    
    // set default auth handler at dispatcher level
    $dispatcher->addHandler(
      Status::UNAUTHORIZED,
      function($result) {
          // display auth page etc.
      }
    );
    
  • Examples of extension, (*64)

    Validation of a parameter value,, (*65)

    $route->addExtension(
      function($result) {
          $id = (int) $result->getParameter('id');
          if ($id > 1000) { // not allowed
              $result->setStatus(Status::PRECONDITION_FAILED);
              return false;
          }
          return true;
      },
      Route::BEFORE_ROUTE // before execute route handler
    );
    

    Statistics for a route collection, (*66)

    $collector->addExtension(
      function($result) {
          // collect statistics
      },
      Collector::BEFORE_COLL // before collector match
    )->addExtension(
      function($result) {
          // collect statistics
      },
      Collector::AFTER_COLL // after a successful match
    );
    
  • Extension stages, (*67)

    Three types of stages, dispatcher level, collector level and route level. List of all stages in the order of execution., (*68)

    • Dispatcher::BEFORE_MATCH before matching starts, (*69)

    • Collector::BEFORE_COLL before matching in a collector, (*70)

    • Collector::AFTER_COLL after a successful match in the collector, (*71)

    • Dispatcher::AFTER_MATCH after a successful match at dispatcher level, (*72)

    • Dispatcher::BEFORE_DISPATCH after a sucessful match, before dispatching to any handler, (*73)

    • Route::BEFORE_ROUTE before executing handler(route's or collector's) for this route, (*74)

    • Route::AFTER_ROUTE after handler successfully executed, (*75)

    • Dispatcher::AFTER_DISPATCH back to dispatcher level, after handler executed successfully, (*76)

    • Dispatcher::BEFORE_DEFAULT match failed or no handler found for the matching route, before execute dispatcher's default handler, (*77)

    • Dispatcher::AFTER_DEFAULT after dispatcher's default handler executed, (*78)

Filters

  • Filter usage, (*79)

    Sometimes, user may want to look at other information before deciding on how to dispatch. Extensions is one way of doing this. But addFilter() of the $route object is a more appropriate way at route level., (*80)

    // match against $_SERVER, $_REQUEST, $_SESSION, $_COOKIE etc.
    $route = (new Route('GET', '/user/list/{$id}', 'handler1'))
      ->addFilter('server.server_name', '(m|www).phossa.com')
      ->addFilter('cookie.vote_status', 'voted');
    

    Even closure is supported, (*81)

    // closure takes the value from $_SERVER['SERVER_NAME'] as input
    $route->addFilter('server.server_name', function($value) {
      switch($value) {
        case 'a1.phossa.com':
        case 'b2.phossa.com':
            return true;
        default:
            return false; // always return a bool
      }
    });
    
  • Difference with extension, (*82)

    Filters are used during the matching process, if filtering failed for the route, the matching process will still try the next route., (*83)

    While route level extensions are executed after a successful match and just before execution of the handler., (*84)

Debugging

Sometimes, you need to know what went wrong., (*85)

php $dispatcher->setDebugMode(true)->setDebugger($logger);, (*86)

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., (*87)

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., (*88)

  • Query Parameter Routing (QPR), (*89)

    The routing info is directly embedded in the URL query. The advantage of this scheme is fast and clear., (*90)

    http://servername/path/?r=controller-action-id-1-name-nick
    

    This strategy is implemented in Phossa\Route\Collector\CollectorQPR class., (*91)

  • Parameter Pairs Routing (PPR), (*92)

    Using parameter and value pairs as follows,, (*93)

    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,, (*94)

    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., (*95)

    http://servername/path/controller-action-id-1-name-nick
    

    This strategy is implemented in Phossa\Route\Collector\CollectorPPR class., (*96)

  • Regular Expression Routing (RER), (*97)

    Regular expression based routing is the default routing strategy for this library and implemented in Phossa\Route\Collector\Collector class., (*98)

    // created with default RER collector
    $dispatcher = new Dispatcher();
    
    // add supprot for legacy query parameter routing
    $dispatcher->addCollector(new CollectorQPR());
    

Regex matching algorithms

Different regex matching algorithms can be used with the RER collector., (*99)

  • FastRoute algorithm, (*100)

    This Group Count Based algorithm is implemented in Phossa\Route\Regex\ParserGcb class and explained in detail in this article "Fast request routing using regular expressions"., (*101)

    phossa-route uses this algorithm by default., (*102)

  • Standard algorithm, (*103)

    This algorithm is developed by phossa-route and a little bit slower than the fastRoute GCB algorithm. It is implemented in Phossa\Route\Regex\ParserStd class., (*104)

    Use this standard algorithm,, (*105)

    use Phossa\Route\Dispatcher;
    use Phossa\Route\Regex\ParserStd;
    use Phossa\Route\Collector\Collector;
    
    // use standard algorithm
    $dispatcher = new Dispatcher(new Collector(new ParserStd));
    
  • Comments on routing algorithms, (*106)

    • 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., (*107)

    • 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., (*108)

Dependencies

  • PHP >= 5.4.0, (*109)

  • phossa/phossa-shared >= 1.0.6, (*110)

  • phossa/phossa-logger >= 1.0.0 if you are using debugging, (*111)

License

MIT License, (*112)

Appendix

  • Performance, (*113)

    • Worst-case matching

    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., (*114)

    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., (*115)

    "Parameter Pairs Routing (PPR)" is fastest and used as baseline., (*116)

    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
    • First route matching

    This benchmark tests how quickly each router can match the first route. 1,000 routes each with 8 arguments., (*117)

    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., (*118)

    Note Both FastRoute and Phroute implement a static route table, so they are fast at the first route matching (which is a static route), (*119)

    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, (*120)

    Setup URL rewriting to do routing with index.php, (*121)

    • 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, (*122)

    <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, (*123)

    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., (*124)

    • Network level routing

    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., (*125)

    • Web server routing

    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., (*126)

    For example, if your server goes down for maintenance, you may replace the .htaccess file as follows,, (*127)

    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]
    
    • App level routing

    It solves much more complicated issues, and much more flexible., (*128)

    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., (*129)

The Versions

13/06 2016

dev-master

9999999-dev https://github.com/phossa/phossa-route

A fast and full-fledged routing libraray for PHP

  Sources   Download

MIT

The Requires

 

The Development Requires

phossa

13/06 2016

1.0.2

1.0.2.0 https://github.com/phossa/phossa-route

A fast and full-fledged routing libraray for PHP

  Sources   Download

MIT

The Requires

 

The Development Requires

phossa

16/03 2016

1.0.1

1.0.1.0 https://github.com/phossa/phossa-route

A fast and full-fledged routing libraray for PHP

  Sources   Download

MIT

The Requires

 

The Development Requires

phossa

15/03 2016

1.0.0

1.0.0.0 https://github.com/phossa/phossa-route

A fast and full-fledged routing libraray for PHP

  Sources   Download

MIT

The Requires

 

The Development Requires

phossa