Defining routes¶
One of the main goals of the front controller pattern is to handle a request by delivering the execution to a specific controller that will process the incoming request.
The ability to determine witch controller will handle the incoming request is done by a Router.
The Router has a collection of Route s with request target patterns and the corresponding Controller’s names, handler method and arguments to be called by the Dispatcher.
The RouterMiddleware will try to match the incoming request against the Route ‘s collection to determine the one that will be used by the dispatcher.
Create the RouterContainer¶
Important
If you have created your project using the project template form Getting started you can skip the creation of the RouterContainer as this is already done. There’s also a routes file already created in config/routes.yml that you will need to edit to match your requirements.
To create a RouterContainer you need to do the following:
use Aura\Router\RouterContainer;
use Slick\WebStack\Http\Router\Builder\RouteFactory;
use Slick\WebStack\Http\Router\RouteBuilder;
use Slick\WebStack\Http\RouterMiddleware;
use Slick\WebStack\Router\Parsers\PhpYmlParser;
$routeBuilder = new RouteBuilder(__DIR__.'/routes.yml', new PhpYmlParser(), new RouteFactory());
$routerContainer = new RouterContainer();
$routeBuilder->register($routerContainer);
It not so simple, I admit! But putting it in simple words you need a file with your route definitions, a parser for that file (YAML in this case) and a route factory that will create Routes from the file definitions.
Then you register it as a route builder in the routes container.
Every time you try to match a request it will recreate the routes you define before it.
Note
We create a simple route builder to the very well done Aura.Router package. The Route, Matcher and RouterContainer are objects that we use as is from that package.
Defining routes¶
Routes are defined in a routes file. A simple example could look like this:
routes:
home:
method: GET
path: /
defaults:
action: home
catchall:
allows: [POST, GET]
path: "{/controller,action}"
wildcard: args
defaults:
namespace: Controller
action: index
controller: pages
Default values¶
The defaults entry is where we define the properties that the dispatcher will use to handle the request. Each route has a defaults entry or it can inherit from the global defaults.
There are 3 mandatory keys in this entry:
- namespace: The namespace where your controller class lives in;
- controller: The controller name. This string will be converted to a regular class name.
- pages will be converted to Pages;
- my-pages will be converted to MyPages;
- otherPages will be converted to OtherPages;
- action: The method that will handle the request inside the controller
- index will be used as is;
- filtered-index will be converted to filteredIndex
Lets take a look to the home route defined in the example file:
routes:
home:
method: GET
path: /
defaults:
action: home
It only defines the action default key but when it matches the result controller and method to be called will be
Controller\Pages::home();
Route list (router)¶
The route list or router is a collection of named routes that are defined in the routes entry.
Important
The order in witch you define the routes in the routes file is very important. The matcher will iterate over the collection and will return the first match. So you need to place the more generic definition at the bottom and the more specific ones at the top.
Route definition¶
A route has the following keys:
- path: The pattern that will be used to match against the request target;
- method: The request method. One of GET, POST, PATCH, PUT, DELETE, HEAD…
- defaults: Information that will be used to dispatch the request;
- allows: Used to define more then one method. Example [GET, POST];
- auth: A key value list of properties that can be used for authentication proposes;
- tokens: A key value list of properties for placeholder token names and regexes;
- accepts: A list of content types that the route handler can be expected to return.;
- host: To limit a route to specific hosts;
- wildcard: To allow arbitrary trailing path segments on a route;
Placeholder tokens¶
When you add a {token} placeholer in the path, it uses a default regular expression of ([^/]+). Essentially, this matches everything except a slash, which of course indicates the next path segment.
To define custom regular expressions for placeholder tokens, use the tokens method.
routes:
blog.read:
method: GET
path: /blog/{id}{format}
tokens:
id: '\d+'
format: '(\.[^/]+)?'
defaults:
format: '.html'
The Route object does not predefine any tokens for you. One that you may find useful is a {format} token, to specify an optional dot-format extension at the end of a file name.
If no default value is specified for a placeholder token, the corresponding attribute value will be null. To set your own default values, add it to the defaults entry.
Optional placeholder tokens¶
Sometimes it is useful to have a route with optional placeholder tokens for attributes. None, some, or all of the optional values may be present, and the route will still match.
To specify optional attributes, use the notation {/attribute1,attribute2,attribute3} in the path. For example:
routes:
archive:
method: GET
path: /archive{/year,month,day}
tokens:
year: '\d{4}'
month: '\d{2}'
day: '\d{2}'
Note that the leading slash separator is inside the placeholder token, not outside.
With that, the following paths will all match the ‘archive’ route, and set the attribute values accordingly:
/archive : ['year' => null, 'month' => null, 'day' = null]
/archive/1979 : ['year' => '1979', 'month' => null, 'day' = null]
/archive/1979/11 : ['year' => '1979', 'month' => '11', 'day' = null]
/archive/1979/11/07 : ['year' => '1979', 'month' => '11', 'day' = '07']
Important
Optional attributes are sequentially optional. This means that, in the above example, you cannot have a “day” without a “month”, and you cannot have a “month” without a “year”. You can have only one set of optional attributes in a route path. Optional attributes belong at the end of a route path. Placing them elsewhere may result in unexpected behavior.
Wildcard Attributes¶
Sometimes it is useful to allow the trailing part of the path be anything at all. To allow arbitrary trailing path segments on a route, add the wildcard entry. This will let you specify the attribute name under which the arbitrary trailing values will be stored.
routes:
wild:
method: GET
path: /wild
wildcard: card
All slash-separated path segments after the /wild path will be captured as an array in the in wildcard attribute. For example:
/wild : ['card' => []]
/wild/foo : ['card' => ['foo']]
/wild/foo/bar : ['card' => ['foo', 'bar']]
/wild/foo/bar/baz : ['card' => ['foo', 'bar', 'baz']]
Wildcards as arguments¶
There is a special case that you can use the wildcard entry to pass arguments to the calling controller handler method:
routes:
catchall:
allows: [POST, GET]
path: "{/controller,action}"
wildcard: args
A request with the target /posts/read/23 will be dispatched as:
Controller\Posts::read(23);
Nested definition files¶
You can organize your route definitions in multiple files that you can add to the main routes file.
For example: if you want to have a group of route definitions for a blog resource you can do like this:
routes:
blog: blog/routes
home:
method: GET
path: /
defaults:
action: home
catchall:
allows: [POST, GET]
path: "{/controller,action}"
wildcard: args
defaults:
namespace: Controller
action: index
controller: pages
Please note the route named blog. It has just the name of the routes file to import into that position. The RouteBuilder will look for the file in config/blog/routes.yml and it will throw an exception if the file is not found.
The config/blog/routes.yml could be something like:
blog.read:
method: GET
path: /blog/{id}{format}
tokens:
id: '\d+'
format: '(\.[^/]+)?'
defaults:
format: '.html'
Note
Nested files feature is only available with version v1.2.0 or higher.