Routing

The icanboogie/routing package handles URL rewriting in native PHP. A request is mapped to a route, which in turn gets dispatched to a controller, and possibly an action. If the process is successful a response is returned. Events are fired during the process to allow event hooks to alter the request, the route, the controller, or the response.

Route definitions

A route definition is an array, which may be created with the following keys:

A route definition is considered valid when the RouteDefinition::PATTERN parameter is defined along one of RouteDefinition::CONTROLLER or RouteDefinition::LOCATION. PatternNotDefined is thrown if RouteDefinition::PATTERN is missing, and ControllerNotDefined is thrown if both RouteDefinition::CONTROLLER and RouteDefinition::LOCATION are missing.

Note: You can add any parameter you want to the route definition, they are used to create the route instance, which might be useful to provide additional information to a controller. Better use a custom route class though.

Route patterns

A pattern is used to match a URL with a route. Placeholders may be used to match multiple URL to a single route and extract its parameters. Three types of placeholder are available:

Additionally, the joker character *—which can only be used at the end of a pattern—matches anything. e.g. /articles/123* matches /articles/123 and /articles/123456 as well.

Finally, constraints RegEx are extended with the following:

You can use them in any combination:

Route controller

The RouteDefinition::CONTROLLER key specifies the callable to invoke, or the class name of a callable. An action can be specified with RouteDefinition::ACTION and if the callable uses ActionTrait the call will be mapped automatically to the appropriate method.

Controllers can also be defined as service references when the icanboogie/service package is used.

Route collections

A RouteCollection instance holds route definitions and is used to create Route instances. A route dispatcher uses an instance to map a request to a route. A route collection is usually created with an array of route definitions, which may come from configuration fragments, RouteMaker, or an expertly crafted array. After the route collection is created it may be modified by using the collection as a array, or by adding routes using one of the supported HTTP methods. Finally, a collection may be created from another using the filter() method.

Defining routes using configuration fragments

If the package is bound to ICanBoogie using icanboogie/bind-routing, routes can be defined using routes configuration fragments. Refer to icanboogie/bind-routing documentation to learn more about this feature.

<?php

use ICanBoogie\Routing\RouteCollection;

// …

$routes = new RouteCollection($app->configs['routes']);
# or
$routes = $app->routes;

Defining routes using offsets

Used as an array, routes can be defined by setting/unsetting the offsets of a RouteCollection.

<?php

use ICanBoogie\HTTP\Request;
use ICanBoogie\Routing\RouteCollection;
use ICanBoogie\Routing\RouteDefinition;

$routes = new RouteCollection;

$routes['articles:index'] = [

    RouteDefinition::PATTERN => '/articles',
    RouteDefinition::CONTROLLER => ArticlesController::class,
    RouteDefinition::ACTION => 'index',
    RouteDefinition::VIA => Request::METHOD_GET

];

unset($routes['articles:index']);

Defining routes using HTTP methods

Routes may be defined using HTTP methods, such as get or delete.

<?php

use ICanBoogie\HTTP\Request;
use ICanBoogie\Routing\RouteCollection;
use ICanBoogie\Routing\RouteDefinition;

$routes = new RouteCollection;
$routes->any('/', function(Request $request) { }, [ RouteDefinition::ID => 'home' ]);
$routes->any('/articles', function(Request $request) { }, [ RouteDefinition::ID => 'articles:index' ]);
$routes->get('/articles/new', function(Request $request) { }, [ RouteDefinition::ID => 'articles:new' ]);
$routes->post('/articles', function(Request $request) { }, [ RouteDefinition::ID => 'articles:create' ]);
$routes->delete('/articles/<nid:\d+>', function(Request $request) { }, [ RouteDefinition::ID => 'articles:delete' ]);

Filtering a route collection

Sometimes you want to work with a subset of a route collection, for instance the routes related to the admin area of a website. The filter() method filters routes using a callable filter and returns a new RouteCollection.

The following example demonstrates how to filter index routes in an "admin" namespace. You can provide a closure, but it's best to create filter classes that you can extend and reuse:

<?php

class AdminIndexRouteFilter
{
    /**
     * @param array $definition A route definition.
     * @param string $id A route identifier.
     * 
     * @return bool
     */
    public function __invoke(array $definition, $id)
    {
        return strpos($id, 'admin:') === 0 && !preg_match('/:index$/', $id);
    }
}

$filtered_routes = $routes->filter(new AdminIndexRouteFilter);

Mapping a path to a route

Routes are mapped using a RouteCollection instance. A HTTP method and a namespace can optionally be specified to determine the route more accurately. The parameters captured from the routes are stored in the $captured variable, passed by reference. If the path contains a query string, it is parsed and stored under __query__ in $captured.

<?php

use ICanBoogie\HTTP\Request;

$home_route = $routes->find('/?singer=madonna', $captured);
var_dump($captured);   // [ '__query__' => [ 'singer' => 'madonna' ] ]

$articles_delete_route = $routes->find('/articles/123', $captured, Request::METHOD_DELETE);
var_dump($captured);   // [ 'nid' => 123 ]

Route

A route is represented by a Route instance. It is usually created from a definition array and contains all the properties of its definition.

<?php

$route = $routes['articles:show'];
echo get_class($route); // ICanBoogie\Routing\Route;

A route can be formatted into a relative URL using its format() method and appropriate formatting parameters. The method returns a FormattedRoute instance, which can be used as a string. The following properties are available:

<?php

$route = $routes['articles:show'];
echo $route->pattern;      // /articles/:year-:month-:slug.html

$url = $route->format([ 'year' => '2014', 'month' => '06', 'slug' => 'madonna-queen-of-pop' ]);
echo $url;                 // /articles/2014-06-madonna-queen-of-pop.html
echo get_class($url);      // ICanBoogie\Routing\FormattedRoute
echo $url->absolute_url;   // https://icanboogie.org/articles/2014-06-madonna-queen-of-pop.html

$url->route === $route;    // true

You can format a route using a record, or any other object, as well:

<?php

$record = $app->models['articles']->one;
$url = $routes['articles:show']->format($record);

Assigning a formatting value to a route

The assign() method is used to assign a formatting value to a route. It returns an updated clone of the route which can be formatted without requiring a formatting value. This is very helpful when you need to pass around an instance of a route that is ready to be formatted.

The following example demonstrates how the assign() method can be used to assign a formatting value to a route, that can later be used like a URL string:

<?php

use ICanBoogie\Routing\RouteCollection;
use ICanBoogie\Routing\RouteDefinition;

$routes = new RouteCollection([

    'article:show' => [

        RouteDefinition::PATTERN => '/articles/<year:\d{4}>-<month:\d{2}>.html',
        RouteDefinition::CONTROLLER => ArticlesController::class,
        RouteDefinition::ACTION => 'show'

    ]

]);

$route = $routes['article:show']->assign([ 'year' => 2015, 'month' => '02' ]);
$routes['article:show'] === $routes['article:show'];   // true
$route === $routes['article:show'];                    // false
$route->formatting_value;                              // [ 'year' => 2015, 'month' => 02 ]
$route->has_formatting_value;                          // true

echo $route;
// /articles/2015-02.html
echo $route->absolute_url;
// https://icanboogie.org/articles/2015-02.html
echo $route->format([ 'year' => 2016, 'month' => 10 ]);
// /articles/2016-10.html

Note: Assigning a formatting value to an assigned route creates another instance of the route. Also, the formatting value is reset when an assigned route is cloned.

Whether a route has an assigned formatting value or not, the format() method still requires a formatting value, it does not use the assign formatting value. Thus, if you want to format a route with its assigned formatting value, use the formatting_value property:

<?php

echo $route->format($route->formatting_value);

Exceptions

The exceptions defined by the package implement the ICanBoogie\Routing\Exception interface, so that they are easy to recognize:

<?php

try
{
    // …
}
catch (\ICanBoogie\Routing\Exception $e)
{
    // a routing exception
}
catch (\Exception $e)
{
    // another type of exception
}

The following exceptions are defined:

Helpers

The following helpers are available:

Patching helpers

Helpers can be patched using the Helpers::patch() method.

The following code demonstrates how routes can start with the custom path "/my/application":

<?php

use ICanBoogie\Routing;

$path = "/my/application";

Routing\Helpers::patch('contextualize', function($str) use($path) {

    return $path . $str;

});

Routing\Helpers::patch('decontextualize', function($str) use($path) {

    if (strpos($str, $path . '/') === 0)
    {
        $str = substr($str, strlen($path));
    }

    return $str;

});