Routing
Routing is the process of mapping URL's to Controller and actions. In the introduction we defined a new custom route
for our TeamController
mapping any teams
URL to our TeamController
If you're using the cms
module with and dealing with Page
objects then for your custom Page Type
controllers you
would extend ContentController
or PageController
. You don't need to define the routes value as the cms
handles
routing.
These routes by standard, go into a routes.yml
file in your applications _config
folder alongside your other
Configuration information.
# app/_config/routes.yml
---
Name: approutes
After:
- '#rootroutes'
- '#coreroutes'
---
SilverStripe\Control\Director:
rules:
'teams//$Action/$ID/$Name': 'App\Control\TeamController'
'player/': 'App\Control\PlayerController'
'': 'App\Control\HomeController'
To understand the syntax for the routes.yml
file better, read the Configuration documentation.
Parameters
SilverStripe\Control\Director:
rules:
'teams//$Action/$ID/$Name': 'App\Control\TeamController'
This route has defined that any URL beginning with team
should create, and be handled by a TeamController
instance.
It also contains 3 parameters
or params
for short. $Action
, $ID
and $Name
. These variables are placeholders
which will be filled when the user makes their request. Request parameters are available on the HTTPRequest
object
and able to be pulled out from a controller using $this->getRequest()->param($name)
.
All Controllers have access to $this->getRequest()
for the request object and $this->getResponse()
for the response.
Here is what those parameters would look like for certain requests
// GET /teams/
print_r($this->getRequest()->params());
// Array
// (
// [Action] => null
// [ID] => null
// [Name] => null
// )
// GET /teams/players/
print_r($this->getRequest()->params());
// Array
// (
// [Action] => 'players'
// [ID] => null
// [Name] => null
// )
// GET /teams/players/1
print_r($this->getRequest()->params());
// Array
// (
// [Action] => 'players'
// [ID] => 1
// [Name] => null
// )
You can also fetch one parameter at a time.
// GET /teams/players/1/
echo $this->getRequest()->param('ID');
// returns '1'
URL patterns
The RequestHandler class will parse all rules you specify against the following patterns. The most specific rule will be the one followed for the response.
A rule must always start with alphabetical ([A-Za-z]) characters or a $Variable declaration
Pattern | Description |
---|---|
$ | Param Variable - Starts the name of a parameter variable, it is optional to match this unless ! is used |
! | Require Variable - Placing this after a parameter variable requires data to be present for the rule to match |
// | Shift Point - Declares that only variables denoted with a $ are parsed into the $params AFTER this point in the regex |
'teams/$Action/$ID/$OtherID': 'App\Control\TeamController'
# /teams/
# /teams/players/
# /teams/
Standard URL handler syntax. For any URL that contains 'team' this rule will match and hand over execution to the
matching controller. The TeamController
is passed an optional action, id and other id parameters to do any more
decision making.
'teams/$Action!/$ID!/': 'App\Control\TeamController'
This does the same matching as the previous example, any URL starting with teams
will look at this rule but both
$Action
and $ID
are required. Any requests to team/
will result in a 404
error rather than being handed off to
the TeamController
.
'admin/help//$Action/$ID: 'AdminHelp'
Match an URL starting with /admin/help/
, but don't include /help/
as part of the action (the shift point is set to
start parsing variables and the appropriate controller action AFTER the //
).
Wildcard URL patterns
As of Silverstripe CMS 4.6 there are two wildcard patterns that can be used. $@
and $*
. These parameters can only be used
at the end of a URL pattern, any further rules are ignored.
Inspired by bash variadic variable syntax there are two ways to capture all URL parameters without having to explicitly specify them in the URL rule.
Using $@
will split the URL into numbered parameters ($1
, $2
, ..., $n
). For example:
namespace App\Control;
use SilverStripe\Control\Controller;
class StaffController extends Controller
{
private static $url_handlers = [
'staff/$@' => 'index',
];
public function index($request)
{
// GET /staff/managers/bob
// managers
$request->latestParam('$1');
// bob
$request->latestParam('$2');
}
}
Alternatively, if access to the parameters is not required in this way then it is possible to use $*
to match all
URL parameters but not collect them in the same way:
namespace App\Control;
use SilverStripe\Control\Controller;
class StaffController extends Controller
{
private static $url_handlers = [
'staff/$*' => 'index',
];
public function index($request)
{
// GET /staff/managers/bob
// managers/bob
$request->remaining();
}
}
URL handlers
You must use the $url_handlers static array described here if your URL
pattern does not use the Controller class's default pattern of
$Action//$ID/$OtherID
. If you fail to do so, and your pattern has more than
2 parameters, your controller will throw the error "I can't handle sub-URLs of
a class name object" with HTTP status 404.
In the above example the URLs were configured using the Director rules in the routes.yml file. Alternatively
you can specify these in your Controller class via the $url_handlers static array. This array is processed by the
RequestHandler at runtime once the Controller
has been matched.
This is useful when you want to provide custom actions for the mapping of teams/*
. Say for instance we want to respond
coaches
, and staff
to the one controller action payroll
.
// app/src/Control/TeamController.php
namespace App\Control;
use SilverStripe\Control\Controller;
class TeamController extends Controller
{
private static $allowed_actions = [
'payroll',
];
private static $url_handlers = [
'staff/$ID/$Name' => 'payroll',
'coach/$ID/$Name' => 'payroll',
];
// ...
}
The syntax for the $url_handlers
array users the same pattern matches as the YAML
configuration rules.
Now let’s consider a more complex example from a real project, where using
$url_handlers is mandatory. In this example, the URLs are of the form
http://example.org/feed/go/
, followed by 5 parameters. The PHP controller
class specifies the URL pattern in $URL_handlers
. Notice that it defines 5
parameters.
namespace App\Control;
use SilverStripe\CMS\Controllers\ContentController;
class FeedController extends ContentController
{
private static $allowed_actions = ['go'];
private static $url_handlers = [
'go/$UserName/$AuthToken/$Timestamp/$OutputType/$DeleteMode' => 'go',
];
public function go()
{
$this->validateUser(
$this->getRequest()->param('UserName'),
$this->getRequest()->param('AuthToken')
);
/* more processing goes here */
}
}
The YAML rule, in contrast, is simple. It needs to provide only enough information for the framework to choose the desired controller.
SilverStripe\Control\Director:
rules:
'feed': 'App\Control\FeedController'
Root URL handlers
In some cases, the Director rule covers the entire URL you intend to match, and you simply want the controller to respond to a 'root' request. This request will automatically direct to an index()
method if it exists on the controller, but you can also set a custom method to use in $url_handlers
with the '/'
key:
namespace App\Control;
use SilverStripe\Control\Controller;
class BreadAPIController extends Controller
{
private static $allowed_actions = [
'getBreads',
'createBread',
];
private static $url_handlers = [
'GET /' => 'getBreads',
'POST /' => 'createBread',
];
// ...
}
In Silverstripe CMS versions prior to 4.6, an empty key (''
) must be used in place of the '/'
key. When specifying an HTTP method, the empty string must be separated from the method (e.g. 'GET '
). The empty key and slash key are also equivalent in Director rules.
Nested request handlers
Nested RequestHandler
routing is used extensively in the CMS and is used to create URL endpoints without YAML configuration. Nesting is done by returning a RequestHandler
from an action method on another RequestHandler
, usually a Controller
.
RequestHandler
is the base class for other classes that can handle HTTP requests such as Controller
, FormRequestHandler
(used by Form
) and FormField
.
How it works
Director::handleRequest()
begins the URL parsing process by parsing the start of the URL and workng out which request handler to use by looking in routes set in YAML config under Director.rules
.
When a request handler matching the first portion of the URL is found, the handleRequest()
method on the matched request handler is called. This passes control to the matched request handler and the next portion of the URL is processed.
From there regular request handling occurs and the URL will be checked to see if it matches $allowed_actions
on the RequestHandler
, possibly routed via $url_handlers
. If an $allowed_action
(i.e. method on the RequestHandler
) is matched and that method returns a request handler, then control will be passed to this nested request handler and the next portion of the URL is processed.
Example of a nested request handler being returned in an action method
Using the code below, navigating to the URL /one/two/hello
will return a response with a body of "hello"
// app/_config/routes.yml
SilverStripe\Control\Director:
rules:
'one': 'App\Control\RequestHandlerOne'
// app/src/Control/RequestHandlerOne.php
namespace App\Control;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
class RequestHandlerOne extends Controller
{
// ...
private static $allowed_actions = [
'two',
];
public function two(HTTPRequest $request)
{
return RequestHandlerTwo::create();
}
}
// app/src/Control/RequestHandlerTwo.php
namespace App\Control;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
class RequestHandlerTwo extends Controller
{
// ...
private static $allowed_actions = [
'hello',
];
public function hello(HTTPRequest $request)
{
return HTTPResponse::create()->setBody('hello');
}
}
How RequestHandler
and Form
work together
Form
does not extend RequestHandler
, instead it implements the HasRequestHandler
interface which defines a method getRequestHandler()
. Form::getRequestHandler()
returns a FormRequestHandler
which is a subclass of RequestHandler
.
Request handlers and implementors of HasRequestHandler
are treated the same because they will both end up calling handleRequest()
on the appropriate request handler.
The FormRequestHandler.url_handlers
configuration property includes an entry 'field/$FieldName!' => 'handleField'
which allows it to handle requests to form fields on the form. FormRequestHandler::handleField()
will find the form field matching $FieldName
and return it. Control is then passed to the returned form field.
FormField
extends RequestHandler
, which means that form fields are able to handle HTTP requests and they have their own $allowed_actions
configuration property. This allows form fields to define their own AJAX endpoints without having to rely on separately routed RequestHandler
implementations.
Example of an AJAX form field that uses nested request handlers
The AJAX request performed by the "Viewer groups" dropdown in asset admin has an endpoint of /admin/assets/fileEditForm/{FileID}/field/ViewerGroups/tree?format=json
That URL ends up passing the request through a series of nested request handlers, which is detailed in the steps below. Unless otherwise stated, the handleRequest()
method is called on the class that has control. Control starts with Director
.
admin
matches a rule in theDirector.rules
YAML configuration property and control is passed toAdminRootController
assets
matches theAssetAdmin.url_segment
property that has a value ofassets
and control is passed toAssetAdmin
fileEditForm/{FileID}
matches'fileEditForm/$ID' => 'fileEditForm'
inAssetAdmin.url_handlers
so theAssetAdmin::fileEditForm()
method is calledAssetAdmin::fileEditForm()
returns aForm
scaffolded for theFile
matching theID
and control is passed to the returnedForm
Form::getRequestHandler()
will be called on theForm
and control is passed to theFormRequestHandler
that is returnedfield/ViewerGroups
matches'field/$FieldName!' => 'handleField'
inFormRequestHandler.url_handlers
, soFormRequestHandler::handleField()
is calledFormRequestHandler::handleField()
finds theViewerGroups
field in theForm
which is aTreeMultiselectField
that extendsTreeDropdownField
and control is passed to the fieldtree
matchestree
inTreeDropdownField.allowed_actions
, soTreeDropdownField::tree()
is calledTreeDropdownField::tree()
returns anHTTPResponse
with its body containing JSON
Related lessons
Links
- Controller API documentation
- Director API documentation
- Example routes: framework
- Example routes: CMS