The Router is an important, powerful and frequently used piece of CakePHP. It is not without its problems though. For 3.0, I've spent sometime fixing many of the problems in router and adding missing functionality.
### 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
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.
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.
### Prefixed actions
Prefixed actions have historically been implemented as prefixes to method names. I feel this has a few drawbacks:
* Action names must be munged and combined with prefixes to create callable methods names.
* Prefixed actions have special code to protect them from direct URL access.
* Prefixes have special code to handle them when generating urls.
* Prefixes require 2 parameters in calls to Router::connect().
* Prefixed actions are in the same class as the non-prefixed actions. This creates enormous classes that are generally handling multiple aspects of an application. Having both the admin and non-admin actions in the same class increases the risk of accidentally making mistakes related to privilege escalation. By using components, traits, and inheritance code reuse (the main benefit of having prefixes in the same class) could be achieved.
#### New implementation
Prefixes directly map to separate namespaces inside a plugin/app. A routing key would be used to indicate the prefix. Possible options are `_prefix`, `_ns`, `_namespace`, `prefix`, `namespace`.
<?php
// Route connection (using `prefix`)
Router::connect('/admin/:controller/:action/*', array('prefix' => 'admin'));
// generate a url
Router::url(['controller' => 'posts', 'action' => 'index', 'prefix' => 'admin']);
// The above resolves to.
App\Controller\Admin\PostsController::index()
Prefixes are `Inflector::camelize()` d before being combined into a namespace. This allows for multi-word prefixes. Prefixes in plugins would work in a similar way:
<?php
// Route connection (using `prefix`)
Router::connect(
'/admin/contacts/:controller/:action/*',
['prefix' => 'admin', 'plugin' => 'contacts']
);
// generate a url
Router::url([
'plugin' => 'contacts',
'controller' => 'users',
'action' => 'index',
'prefix' => 'admin'
]);
// The above resolves to.
Contacts\Controller\Admin\UsersController::index()
The `prefix` key could be set to `false` to force a route to generate as a non-prefixed method. The `Routing.prefixes` Configure var would still be available to declaratively configure which prefixes are going to be connected by the default routes.
#### View files
All view files would be located in paths similar to the controllers. So if your controller was `App\Controller\Admin\ContactsController` the view files would be in `App/View/Admin/Contacts/` by convention. Of course you can change this using `Controller::$viewPath`. Prefix controllers with alternate formats would follow the convention of `App/View/Admin/Contacts/{xml,json}/`.
I apologize for the long winded post, if anyone is interested in the changes you can see the branch diff at:
If anyone has any feedback, I'd love to hear it so I can integrate the feedback and merge this into 3.0.
-Mark