New routers needed in the country

Overview of alternative PHP routers
6
Mar

New routers needed in the country

The dominance of the PHP framework silos is coming to an end. Thanks to the increasing popularity of the Composer, PHP developers are finding it easier and easier to build their own framework from many different building blocks and packages. This article provides an overview of some PHP routers that are available as useful alternatives.

Earlier, in the good old days, the PHP full stack frameworks offered their own solutions for every problem in web development. These frameworksilos were a closed ecosystem, even if external packages such as Doctrine or the one or other template engine could be integrated additionally. However, for elementary aspects such as modeling HTTP requests and responses or routing, most PHP frameworks provided their own solutions. This is still the case today, but times have changed.

At that, time it was usually a bit complicated to integrate external packages into a project. It was oftentimes only possible to download the package, unpack it, store it in a folder and integrate it into the autoloading process. If there was a new version, the game started all over again. It’s been a bit more than five years since the first version of Composer was released. Since then, it is no longer a problem to use the Composer to install dependencies and keep them up to date.

Another aspect that contributed to the opening of the closed-loop PHP framework ecosystems was the foundation of PHP-FIG in 2009. The PHP Framework Interoperability Group includes representatives of all well-known PHP frameworks and other PHP open source projects such as Composer, Drupal, Joomla!, Magento or SugarCRM. The group aims to establish standards for development in the PHP world. Initially, the focus was mainly on autoloading and coding styles, but in recent years standards in the form of interfaces have increasingly been created. For example, there are now approved interfaces for logging, caching and modelling HTTP messages; further interfaces are in the works. At the beginning of 2016, many PHP frameworks hurried to implement the PSR-7 standard for HTTP requests and responses in their frameworks.

Discover the program of IPC ’18!

Thanks to this new openness, PHP developers should look beyond their horizons and try something new. In this article we take a look at different routing solutions that have emerged in recent years as alternatives to the established routers of the individual PHP frameworks. In detail, these are:

  • Aura.Router
  • FastRoute
  • klein.php
  • Pux

This article is accompanied by code examples in a GitHub repository. After cloning the repository, it is best to let Composer install the dependencies and thus the individual router packages. You should also set write permissions to the /data/log/ directory. Afterwards you can start the project; it is from now on available on http://0.0.0.0:8080/ in the browser:

$ git clone https://github.com/RalfEggert/phpmagazin.router 
$ cd phpmagazin.router
$ composer install
$ sudo chmod -R 777 data/log/
$ composer serve

 

Routes, handlers and templates used

For all four routers, essentially the same routes, handlers and templates are used in the examples to increase comparability. The only exception are the handlers for the router klein.php, which requires slightly adapted handlers. All routers implement similar routes with different names. But the scheme is the same everywhere:

  • /router/ for a main page GET request
  • /router/id for a GET request of a display page for the ID
  • /router/create for a GET request to display a formular
  • /router/create for a POST request to process a formular

Listing 1 shows the file /handler/other_handlers.php with a renderer function and some handler functions that are used for the individual routes. These handler functions are for demonstration purposes only and do not include full functionality. They are used by the individual routes for which a hit occurred. The Zend framework component zend-diactoros is used for the HTML response to improve comparability. In addition, the renderer function is only intended for demonstration purposes.

In Listing 2, you will find the template for the start page from the file /tpl/home.html as an example. In addition to the HTML markup, it also contains some placeholders. They are replaced accordingly in the renderer function so that the templates can be used for all routers.

<?php
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
$renderer = function ($tpl) {
  $html = implode('', file($tpl));
  html = str_replace('%%router_name%%', ROUTER_NAME, $html);
  $html = str_replace('%%router_route%%', ROUTER_ROUTE, $html);

  return $html;
};

$homeHandler = function () use ($renderer) {
  $tpl = __DIR__.'/../tpl/home.html';

  $html = $renderer($tpl);

  return new HtmlResponse($html);
};

$fileNotFoundHandler = function () use ($renderer) {
  $tpl = __DIR__.'/../tpl/404.html';

  $html = $renderer($tpl);

  return new HtmlResponse($html);
};

$methodNotAllowedHandler = function () use ($renderer) {
  $tpl = __DIR__.'/../tpl/405.html';

  $html = $renderer($tpl);

  return new HtmlResponse($html);
};

$showHandler = function ($request) use ($renderer) {
  if (is_array($request) && isset($request['request'])) {
    $request=$request['request'];
  }

  /** @var ServerRequestInterface $request */
  $id=(int)$request->getAttribute('id');

  $tpl=__DIR__.'/../tpl/show.html';

  $html=$renderer($tpl);
  $html=str_replace('%%id%%',$id, $html);

  return new HtmlResponse($html);
};

$createGetHandler = function () use ($renderer) {
  $tpl=__DIR__.'/../tpl/create-get.html';

  $html=$renderer($tpl);

  return new HtmlResponse($html);
};

$createPostHandler = function ($request) use ($renderer) {
  if (is_array($request) && isset($request['request'])) {
    $request=$request['request'];
  }

  /** @var ServerRequestInterface $request */
  $postData=$request->getParsedBody();
  $title=(string)$postData['title'];

  $tpl = __DIR__.'/../tpl/create-post.html';

  $html=$renderer($tpl);
  $html=str_replace('%%title%%', $title, $html);

  return new HtmlResponse($html);
};

 

<html>
<head>
  <title>PHP Magazin: Neue PHP Router %%router_name%%</title>
  <link rel="stylesheet" href="/css/bootstrap-3.3.7/bootstrap.min.css">
  <link rel="stylesheet" href="/css/bootstrap-3.3.7/bootstrap-theme.min.css">
  <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="container">
  <h1>PHP Magazin: Neue PHP Router</h1>
  <h2>Willkommen zum %%router_name%%</h2>
  <ul>
    <li><a href="%%router_route%%/1">Blogbeitrag 1</a></li>
    <li><a href="%%router_route%%/create">Neuer Blogbeitrag</a></li>
  </ul>
  <a href="/" class="btn btn-success">Übersicht</a>
</div>
</body>
</html>

 

Aura.Router

Aura.Router is part of the Aura for PHP project, a collection of tools for building PHP applications. The current release 3.1.0 tested for this article was released on March 2,2017. Aura.Router has been developed since November 2012 and requires at least PHP 5.5, but is also compatible and tested with all higher versions of PHP. The documentation tells the user how to define and group routes and generate paths. Custom maps, custom matching rules and other topics are also explained. Aura.Router is available at GitHub and can be easily installed via Composer:

$ composer require aura/router

Listing 3 shows the use of Aura.Router. After defining some constants, the autoloading of the composer and the handlers are included. The request to be processed is then prepared using zend-diactoros. Now the routes are defined with the RouterContainer. The new routes are added to the map. The HTTP method to which the route is applied is determined by calling the get () or post () methods. The names of the routers, the path and a handler for processing are passed to the methods. A special feature is the route aura. show because it contains a token that defines the validity for the ID attribute.

<?php
use Aura\Router\RouterContainer;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\ServerRequestFactory;

define('ROUTER_NAME', 'Aura.Router');
define('ROUTER_ROUTE', '/aura');

require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../handler/other_handlers.php';

$request = ServerRequestFactory::fromGlobals(
  $_SERVER,
  $_GET,
  $_POST,
  $_COOKIE,
  $_FILES
);

$routerContainer = new RouterContainer();

$map=$routerContainer->getMap();
$map->get('aura.home', '/aura/', $homeHandler);
$map->get('aura.show', '/aura/{id}', $showHandler)->tokens(
  ['id' => '\d+']
);
$map->get('aura.create.get','/aura/create', $createGetHandler);
$map->post('aura.create.post','/aura/create', $createPostHandler);

$route=$routerContainer->getMatcher()->match($request);

if (!$route) {
  $response=$fileNotFoundHandler();
} else {
  foreach ($route->attributes as $key => $val) {
    $request = $request->withAttribute($key, $val);
  }

  $handler = $route->handler;

  /** @var HtmlResponse $response */
   $response = $handler($request);
}

foreach ($response->getHeaders() as $name => $values) {
  foreach ($values as $value) {
    header(sprintf('%s: %s', $name, $value), false);
  }
}

echo $response->getBody();

The actual routing is done via the Matcher. If no route matches the current request, a handler is used for an error page. Otherwise, the route attributes found are transferred to the request object so that the handlers can access them. Finally, the handler that returns an HTML response is executed. It is returned to the client together with possible headers. The rough procedure is also very similar for the other routers. Defining a route in Aura. router is done as follows:

$map = $routerContainer->getMap();
$map->get('aura.home','/aura/', $homeHandler);
$map->get('aura.show','/aura/{id}', $showHandler)->tokens(['id' => '\d+']);

 

FastRoute

FastRoute is a project created by Nikita Popov and has been developed as an independent package since November 2014. The latest version 1.2.0 was released on February 6,2017. FastRoute requires at least PHP 5.4 and is compatible with all latest versions of PHP. The documentation explains the definition of routes, caching, dispatching and the expansion of FastRoute. The package is also very easy to install using Composer:

$ composer require nikic/fast-route

In Listing 4 you can see the definition and routing process for FastRoute. Here the RouteCollector is used with the GroupCountBasedGenerator, to which the individual routes can be added using the addRoute () method. The HTTP method, the route path and the handler are used as parameters. A name for the route is not set. After defining the route, the GroupCountBasedDispatcher is instantiated, which processes the request using the HTTP method and the URI of the request. As a result, an array of route data is returned. A simple switch () statement can be used to catch 404 and 405 errors. If a hit occurs, the request is also enriched with routing parameters and the handler is executed afterwards. Finally, the HTML response is sent to the client. The definition of a route with FastRoute looks like this:

$routeCollector = new RouteCollector(new Std(), new GroupCountBasedGenerator());
$routeCollector->addRoute('GET','/fast/', $homeHandler);
$routeCollector->addRoute('GET','/fast/{id:\d+}', $showHandler);

 

<?php
use FastRoute\DataGenerator\GroupCountBased as GroupCountBasedGenerator;
use FastRoute\Dispatcher;
use FastRoute\Dispatcher\GroupCountBased as GroupCountBasedDispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std;
use Zend\Diactoros\ServerRequestFactory;

define('ROUTER_NAME','FastRoute');
define('ROUTER_ROUTE','/fast');

require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../handler/other_handlers.php';

$request = ServerRequestFactory::fromGlobals(
  $_SERVER,
  $_GET,
  $_POST,
  $_COOKIE,
  $_FILES
);

/** @var RouteCollector $routeCollector */
$routeCollector = new RouteCollector(
  new Std(), new GroupCountBasedGenerator()
);
$routeCollector->addRoute('GET','/fast/', $homeHandler);
$routeCollector->addRoute('GET','/fast/{id:\d+}', $showHandler);
$routeCollector->addRoute('GET','/fast/create', $createGetHandler);
$routeCollector->addRoute('POST','/fast/create', $createPostHandler);

$dispatcher = new GroupCountBasedDispatcher($routeCollector->getData());

$routeInfo = $dispatcher->dispatch(
  $request->getMethod(), $request->getUri()->getPath()
);

switch ($routeInfo[0]) {
  case Dispatcher::NOT_FOUND:
  $response = $fileNotFoundHandler();

  break;

  case Dispatcher::METHOD_NOT_ALLOWED:
  $response = $methodNotAllowedHandler();

  break;

  case Dispatcher::FOUND:
  default:
  foreach ($routeInfo[2] as $key => $val) {
    $request = $request->withAttribute($key, $val);
  }

  $handler = $routeInfo[1];

  $response = $handler($request);

  break;
}

foreach ($response->getHeaders() as $name => $values) {
  foreach ($values as $value) {
    header(sprintf('%s: %s', $name, $value), false);
  }
}

echo $response->getBody();

 

Klein

The small router, or rather klein.php, is a standalone router package from Chris O’ Hara, which, compared to the other solutions presented so far, also comes with a dispatcher, which takes care of the rendering. The package has been developed since March 2013, requires at least PHP 5.3 and has the lowest requirement of all four routers. The current release 2.1.2 was released on February 4,2017. The documentation and the Wiki contain all the information you need to define routes, route namespaces, route parameters, views and the API of the component. You can install the package using Composer as follows:

$ composer require klein/klein

Listing 5 demonstrates the use of klein.php. Since this router also handles the call of the handlers directly during dispatching, this listing is much shorter than the others. While a response object is returned for the alternatives, the dispatcher takes care of rendering directly. This is because the handlers have to return a string and therefore the rendered HTML. The definition is similar to that of the predecessors. The HTTP method, path and handler can be passed for each route. The optional parameter is defined here directly in the path. Again, the routes are not assigned a name. In summary, with klein.php you can define routes as follows:

$klein = new Klein();
$klein->respond('GET','/klein/',$homeHandler);
$klein->respond('GET','/klein/[i:id]',$showHandler);

 

<?php
use Klein\Klein;
use Zend\Diactoros\ServerRequestFactory;

define('ROUTER_NAME','Klein');
define('ROUTER_ROUTE','/klein');

require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../handler/klein_handlers.php';

$request = ServerRequestFactory::fromGlobals(
  $_SERVER,
  $_GET,
  $_POST,
  $_COOKIE,
  $_FILES
);
$klein = new Klein();
$klein->respond('GET','/klein/', $homeHandler);
$klein->respond('GET','/klein/[i:id]', $showHandler);
$klein->respond('GET','/klein/create', $createGetHandler);
$klein->respond('POST','/klein/create', $createPostHandler);

$klein->dispatch();

 

Pux

The last router in the bundle is called Pux and has been developed since January 2014. Pux comes with an optional controller, and the latest version 2.0.0 has been released in June 2016. This router requires at least PHP 5.4 and even has its own PHP extension for performance enhancement. The documentation explains, among other things, the use of the Mux component for route definition, the use of the optional APC Dispatcher and the optional controller, as well as the Route Executor and the Mux compiler. Of course, this component can also be installed using Composer:

$ composer require corneltek/pux

In Listing 6 you can see Pux in action. The definition of the routes is done with Mux similar to the previous ones. Here too, the HTTP method, path and handler are specified, whereby the route parameters can be defined using an optional array. After defining the routes, dispatching takes place. If a hit occurs, the route attributes can be transferred to the request object. Only then does the RouteExecutor execute the handler of the current route. The definition of a route with Pux and Mux is done in a similar way:

$mux = new Mux();
$mux->get('/pux/', $homeHandler);
$mux->get('/pux/:id', $showHandler, ['require' => ['id' => '\d+']]);

 

<?php
use Pux\Mux;
use Pux\RouteExecutor;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;

define('ROUTER_NAME','Pux');
define('ROUTER_ROUTE','/pux');

require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../handler/other_handlers.php';

$request = ServerRequestFactory::fromGlobals(
  $_SERVER,
  $_GET,
  $_POST,
  $_COOKIE,
  $_FILES
);

$mux = new Mux();
$mux->get('/pux/', $homeHandler);
$mux->get('/pux/:id', $showHandler, ['require' => ['id' => '\d+']]);
$mux->get('/pux/create', $createGetHandler);
$mux->post('/pux/create', $createPostHandler);

$route = $mux->dispatch($request->getUri()->getPath());

if (isset($route[3]['variables']) && isset($route[3]['vars'])) {
  foreach ($route[3]['variables'] as $key) {
    $request = $request->withAttribute($key, $route[3]['vars'][$key]);
  }
}

/** @var Response $response */
$response = RouteExecutor::execute($route, ['request' => $request]);

foreach ($response->getHeaders() as $name => $values) {
  foreach ($values as $value) {
    header(sprintf('%s: %s', $name, $value), false);
  }
}

echo $response->getBody();

 

And the winner is …

When comparing different software solutions, there are usually several winners, depending on how the individual evaluation criteria are weighted. On the one hand, the focus can be on the supported features, on the other hand on the actuality of the component or on the support. However, performance is brought to the fore.

The pure definition of the routes is very similar for all four variants and can be compared very well with each other due to the common handlers and templates as well as the same route structure. The supported features are also very similar, even if they are implemented differently in detail.

For comparison, I have made a simple performance comparison, which certainly does not claim to be 100 percent accurate. The complete process was measured from the definition of the routes via matching to the issue of the response. Since all routes use the same handlers, the comparison is fairly objective. After fifty rounds, it turned out that Pux is slightly ahead and the klein.php router comes in last (Table 1).

Router Aura.Router FastRoute klein.php Pux
Version 3.1.0 1.2.0 2.1.2 2.0.0
Run ∅ 4,287 ms 3,546 ms 5,473 ms 3,394 ms
Table 1: Performance comparison of routers

 

Since performance comparisons cannot be everything, please take into consideration the following notes before you decide on one of the routers:

  • Aura.Router seems to be the least used in GitHub (Stars, Watches, Forks) based on the information. The author takes an exemplary care of the issues and pull requests. With bugs or feature requests, this might be the best chance to have them introduced.
  • FastRoute is the second fastest router in this comparison and is apparently one of the most used routers (see Stars etc. at GitHub). There are currently only a few open issues and PR issues, so the author seems to be very active.
  • klein.php has its own dispatcher and is the slowest in the test. The number of open issues and unprocessed pull requests is also the highest here. Due to the integrated dispatcher, the least code has to be written when defining the routes and carrying out the routing, which makes it easier to use.
  • Pux is the fastest router and offers further performance enhancements with the PHP extension, but has last been updated in June 2016 (updated 02.06.2017). The number of open issues also gives the impression that the author is currently somewhat less active.

If I look at all the parameters, I would prefer to use FastRoute. The mixture of speed and popularity makes this router very attractive. Take a look at them all and compare them yourself. Each developer has to fulfill different decision criteria and possibly also technical specifications, so that the choice for you could surely fall on one of the other routers.

Support in PHP frameworks

There is still the question of a possible integration of these routers into one of the known frameworks. Expressive, the microframework from Zend Framework, supports Aura. router and FastRoute as well as its own router and is therefore the most open. The effort to integrate the other routers into Expressive should be manageable. Lumen, Laravel’s microframework, also relies on FastRoute, exclusively. With all other frameworks, especially the full stack frameworks, however, it looks like they trust in their own solutions.

I hope this comparison helped you with the decision-making process.

Stay tuned!

Behind the Tracks of IPC ’18

PHP Development
Best Practices & Application

Web Development
Web Development & more

Web Architecture
Concepts & Environments

Performance & Security
All about Performance & Security

Agile & DevOps
Agile & DevOps methodologies

JavaScript
All about JavaScript

Testing & Quality
All about Testing & Quality