Router.next - markstory/cakephp GitHub Wiki

The Router is an important, powerful and frequently used piece of CakePHP. It is not without its problems though.

Problems

  • Reverse routing is slow. With many connected routes, reverse routing can be slow, as its a linear search across all connected routes.
  • Named parameters are a mess. They are an unconventional way to build URL's that aren't implemented or supported by any other tools. They break the ideas of 'convention over configuration', as they are not a convention shared anywhere else on the internet. This makes it difficult for other tools to parse and generate. In addition to this, named parameters incur some overhead when routes are matched, as various filtering steps are required.
  • Prefixes are poorly implemented. I feel that prefixes could be better implemented as sub-namespaces. This would result in smaller, easier to test code as admin/other methods are separate and contained.

Named routes

Named routes, even if the names are not entirely unique, can speed up reverse routing. By reducing the set of routes that needs to be traversed. Using a bucketed array, routes with the same name could be put into sets that could be traversed. If no names are provided, a linear search would be done.

Names could be explicit, or generated based on the route template.

<?php
// Explicit name.
Router::connect('/:controller/:action/*', [], ['_name' => '_controller::_action']);
Router::connect('/login', ['controller' => 'users', 'action' => 'login'], ['_name' => 'login']);

// Generated name. Results in 'posts::_action'
Router::connect('/posts/:id/:action', ['controller' => 'posts']);

Generated names use the controller + action to create a name. If a controller action has multiple routes that point to it, those routes are added to a single collection. When reverse routing only the routes for a given plugin.controller::action are considered. In many cases this will only match against one route. Routes also fallback in a reliable order. If ['controller' => 'posts', 'action' => 'index'] was matched the following keys would be attempted:

  • posts:index
  • posts:_action
  • _controller:_action

If no routes are found for any fallbacks, an exception is raised. Plugin routes fallback in a similar fashion but with the Plugin prefix always present. For example:

  • assetcompress.asset:get
  • assetcompress.asset:_action
  • assetcompress._controller:_action
  • _controller:_action

Special route keys

Currently url arrays support a few special keys. The special keys should be expanded to contain the following:

  • ssl - Set to true. Basically a shortcut for _scheme => https. Can be set to false to convert the scheme to http.
  • _scheme - Defaults to the current request's scheme. Would be used for creating https, or webcal links for example.
  • _host - The host to use, defaults to the current host.
  • _port - Defaults to the current requests' port. Would be used for running SSL on un-conventional ports.
  • _full - Set to true to include the full protocol, port and domain. (Defaults to false)
  • _base - Set to false to return an application relative path. Application relative paths are missing the subdirectory the application is running in. This is useful for making sub requests.
  • _ext - The extension used for the url ie. json for .json urls.
  • # - Create fragment identifiers.

All special keys except # are prefixed with a _. This is to prevent overlap with userland route elements. As long as none of the following keys are used generated urls will be domain relative (ie. /dir/controller/action) _scheme, _port, _host, _full.

Reverse Routing examples

<?php
// foo is unknown, and not a route element.
// This url uses the _controller:_action default route.
// so the extra params become a query string parameter.
Router::url(['controller' => 'posts', 'action' => 'index', 'foo' => 'bar']);
// Creates /posts/index?foo=bar

// Create a url for App\Controller\Admin\PostsController.
Router::url([
  'prefix' => 'admin',
  'controller' => 'posts',
  'action' => 'index',
  'lang' => 'en'
]);
// Creates /admin/posts/en/index

// Use a name for the route.
Router::url('login');

Router::url() has two valid signatures:

<?php
Router::url($name, $params);
Router::url($params);

Using named routes lets you more tersely express routes that would require long sets of parameters in normal routing arrays. Additionally using unique names for all routes reduces the time to lookup routes to almost nothing.

Reverse routing could use the following pseudo code:

If the url has a name, and an exact match in the hashtable.
  Loop over each route in the matching set.
    If a route matches, return the generated url
Iterate the fallback route names
  Get matching routes from the hashtable
    Loop over matching routes
      If a route matches, return the generated url.
No matches were found raise an exception.

If no explicit name is used, a name will be generated based on the routing parameters and used to look at a smaller route set. This will speed up route lookups, but has the potential to incorrectly choose routes. In these situations its best to use explicit names for routes.

Once routes have been limited to those matching the controller + action, route matching could follow the following pseudo-code:

Check that all route element exists as keys in the url parameters.
  return false
Check that all the default route elements exist as keys in the url parameters
  return false
For each route key check the conditions. If one fails, 
  return false
If the route is not greedy, and there are numeric keyed values
  return false
Remove route element keys from the url array -> route key values.
Remove numerically indexed values -> passed arguments.
Populate the route template with the route key values.
If there are passed arguments, append those.
Append the remaining url parameters as the query string.

Handling parameters

While named parameters have been removed, passed arguments could live on. Route parameters with numeric keys, would be treated as passed arguments, and appended to the end of greedy routes. Non greedy routes, will not be matched by arrays containing numeric keys. Values with string keys will be treated as query string parameters, and are acceptable for any type of route.

Prefixed actions

See prefixed actions for more information on the planned changes.

Implementation

The implementation of the above could be done using the following class skeletons:

  • Request - contains data about a request, and is set to the router's request stack as each request is created. Requests are used to retrieve data about the current request.
  • Route a single connected route. Knows how to parse incoming url's, and reverse route parameters into url strings.
  • RouteCollection - A collection of routes, maintains the hashtables and ordered lists of routes.
  • Router - Used to provide an friendly API for connecting, parsing, and matching urls.

Other changes proposed

Status

The changes described here have been implemented in 3.0