Routing

Table of Contents

  1. Introduction
  2. Basic Usage
    1. Using Closures
    2. Using Controller Classes
    3. Multiple Methods
    4. Dependency Resolvers
  3. Route Variables
    1. Regular Expressions
    2. Optional Parts
    3. Default Values
  4. Host Matching
  5. Middleware
    1. Middleware Parameters
  6. HTTPS
  7. Named Routes
  8. Route Grouping
    1. Controller Namespaces
    2. Group Middleware
    3. Group Hosts
    4. Group HTTPS
    5. Group Variable Regular Expressions
  9. URL Generators
    1. Generating URLs from Code
    2. Generating URLs from Views
  10. Caching
  11. Missing Routes
  12. Notes

Introduction

So, you've made some page views, and you've written some models. Now, you need a way to wire everything up so that users can access your pages. To do this, you need a Router and controllers. The Router can capture data from the URL to help you decide which controller to use and what data to send to the view. It makes building a RESTful application a cinch.

Basic Usage

Routes require a few pieces of information:

Opulence\Routing\Router supports various methods out of the gate:

Using Closures

For very simple applications, it's probably easiest to use closures as your routes' controllers:

use Opulence\Ioc\Container;
use Opulence\Routing\Dispatchers\ContainerDependencyResolver;
use Opulence\Routing\Dispatchers\MiddlewarePipeline;
use Opulence\Routing\Dispatchers\RouteDispatcher;
use Opulence\Routing\Router;
use Opulence\Routing\Routes\Compilers\Compiler;
use Opulence\Routing\Routes\Compilers\Matchers\HostMatcher;
use Opulence\Routing\Routes\Compilers\Matchers\PathMatcher;
use Opulence\Routing\Routes\Compilers\Matchers\SchemeMatcher;
use Opulence\Routing\Routes\Compilers\Parsers\Parser;

$dispatcher = new RouteDispatcher(
    new ContainerDependencyResolver(new Container()),
    new MiddlewarePipeline()
);
$compiler = new Compiler([new PathMatcher(), new HostMatcher(), new SchemeMatcher()]);
$parser = new Parser();
$router = new Router($dispatcher, $compiler, $parser);
$router->get('/foo', function () {
    return 'Hello, world!';
});

If you need any object like the Request to be passed into the closure, just type-hint it:

use Opulence\Http\Requests\Request;

$router->get('/users/:id', function (Request $request, $id) {
    // $request will be the HTTP request
    // $id will be the path variable
});

Using Controller Classes

Anything other than super-simple applications should probably use full-blown controller classes. They provide reusability, better separation of responsibilities, and more features. Read here for more information about controllers.

Multiple Methods

You can register a route to multiple methods using the router's multiple() method:

$router->multiple(['GET', 'POST'], "MyApp\\MyController@myMethod");

To register a route for all methods, use the any() method:

$router->any("MyApp\\MyController@myMethod");

Dependency Resolvers

Before we dive too deep, let's take a moment to talk about dependency resolvers. They're useful tools that allow our router to automatically instantiate controllers by scanning their constructors for dependencies. Unlike a dependency injection container, a resolver's sole purpose is to resolve dependencies. Binding implementations (like through bootstrappers) is reserved for containers. That being said, it is extremely common for a resolver to use a container to help it resolve dependencies.

Opulence provides an interface for dependency resolvers (Opulence\Routing\Dispatchers\IDependencyResolver). It defines one method: resolve($interface). Opulence provides a resolver (Opulence\Routing\Dispatchers\ContainerDependencyResolver) that uses its container library. However, since the resolver interface is so simple to implement, you are free to use the dependency injection container library of your choice to power your resolver. If you decide to use Opulence's container library and you're not using the entire framework, include the opulence/ioc Composer package.

Route Variables

Let's say you want to grab a specific user's profile page. You'll probably want to structure your URL like "/users/:userId/profile", where ":userId" is the Id of the user whose profile we want to view. Using a Router, the data matched in ":userId" will be mapped to a parameter in your controller's method named "$userId".

Note: This also works for closure controllers. All non-optional parameters in the controller method must have identically-named route variables. In other words, if your method looks like function showBook($authorName, $bookTitle = null), your path must have an :authorName variable. The routes /authors/:authorName/books and /authors/:authorName/books/:bookTitle would be valid, but /authors would not.

Let's take a look at a full example:

use Opulence\Routing\Controller;

class UserController extends Controller
{
    public function showProfile(int $userId)
    {
        return 'Profile for user ' . $userId;
    }
}

$router->get('/users/:userId/profile', "MyApp\\UserController@showProfile");

Calling the path /users/23/profile will return "Profile for user 23".

Regular Expressions

If you'd like to enforce certain rules for a route variable, you may do so in the options array. Simply add a "vars" entry with variable names-to-regular-expression mappings:

$options = [
    'vars' => [
        'userId' => "\d+" // The user Id variable must now be a number
    ]
];
$router->get('/users/:userId/profile', "MyApp\\UserController@showProfile", $options);

Optional Parts

If parts of your route are optional, simply wrap them in []:

$router->get('/books[/authors]', "MyApp\\BookController@showBooks");

This would match both /books and /books/authors.

You can even nest optional parts:

$router->get('/archives[/:year[/:month[/:day]]]', "MyApp\\ArchiveController@showArchives");

Default Values

Sometimes, you might want to have a default value for a route variable. Doing so is simple:

$router->get('/food/:foodName=all', "MyApp\\FoodController@showFood");

If no food name was specified, "all" will be the default value.

Note: To give an optional variable a default value, structure the route variable like [:varName=value].

Host Matching

Routers can match on hosts as well as paths. Want to match calls to a subdomain? Easy:

$options = [
    'host' => 'mail.mysite.com'
];
$router->get('/inbox', "MyApp\\InboxController@showInbox", $options);

Just like with paths, you can create variables from components of your host. In the following example, a variable called $subdomain will be passed into MyApp\SomeController::doSomething():

$options = [
    'host' => ':subdomain.mysite.com'
];
$router->get('/foo', "MyApp\\SomeController@doSomething", $options);

Host variables can also have regular expression constraints, similar to path variables.

Middleware

Routes can run middleware on requests and responses. To register middleware, add it to the middleware property in the route options:

$options = [
    'middleware' => "MyApp\\MyMiddleware" // Can also be an array of middleware
];
$router->get('/books', "MyApp\\MyController@myMethod", $options);

Whenever a request matches this route, MyApp\MyMiddleware will be run.

Middleware Parameters

Opulence supports passing primitive parameters to middleware. To actually specify role, use {Your middleware}::withParameters() in your router configuration:

$options = [
    'middleware' => [RoleMiddleware::withParameters(['role' => 'admin'])]
];
$router->get('/users', "MyController\\MyController@myMethod", $options);

HTTPS

Some routes should only match on an HTTPS connection. To do this, set the https flag to true in the options:

$options = [
    'https' => true
];
$router->get('/users', "MyApp\\MyController@myMethod", $options);

HTTPS requests to /users will match, but non SSL connections will return a 404 response.

Named Routes

Routes can be given a name, which makes them identifiable. This is especially useful for things like generating URLs for a route. To name a route, pass a "name" => "THE_NAME" into the route options:

$options = [
    'name' => 'awesome'
];
$router->get('/users', "MyApp\\MyController@myMethod", $options);

This will create a route named "awesome".

Route Grouping

One of the most important sayings in programming is "Don't repeat yourself" or "DRY". In other words, don't copy-and-paste code because that leads to difficulties in maintaining/changing the code base in the future. Let's say you have several routes that start with the same path. Instead of having to write out the full path for each route, you can create a group:

$router->group(['path' => '/users/:userId'], function (Router $router) {
    $router->get('/profile', "MyApp\\UserController@showProfile");
    $router->delete('', "MyApp\\UserController@deleteUser");
});

Now, a GET request to /users/:userId/profile will get a user's profile, and a DELETE request to /users/:userId will delete a user.

Controller Namespaces

If all the controllers in a route group belong under a common namespace, you can specify the namespace in the group options:

$router->group(['controllerNamespace' => "MyApp\\Controllers"], function (Router $router) {
    $router->get('/users', 'UserController@showAllUsers');
    $router->get('/posts', 'PostController@showAllPosts');
});

Now, a GET request to /users will route to MyApp\Controllers\UserController::showAllUsers(), and a GET request to /posts will route to MyApp\Controllers\PostController::showAllPosts().

Group Middleware

Route groups allow you to apply middleware to multiple routes:

$router->group(['middleware' => "MyApp\\Authenticate"], function (Router $router) {
    $router->get('/users/:userId/profile', "MyApp\\UserController@showProfile");
    $router->get('/posts', "MyApp\\PostController@showPosts");
});

The Authenticate middleware will be executed on any matched routes inside the closure.

Group Hosts

You can filter by host in router groups:

$router->group(['host' => 'google.com'], function (Router $router) {
    $router->get('/', "MyApp\\HomeController@showHomePage");
    $router->group(['host' => 'mail.'], function (Router $router) {
        $router->get('/', "MyApp\\MailController@showInbox");
    });
});

Note: When specifying hosts in nested router groups, the inner groups' hosts are prepended to the outer groups' hosts. This means the inner-most route in the example above will have a host of "mail.google.com".

Group HTTPS

You can force all routes in a group to be HTTPS:

$router->group(['https' => true], function (Router $router) {
    $router->get('/', "MyApp\\HomeController@showHomePage");
    $router->get('/books', "MyApp\\BookController@showBooksPage");
});

Note: If the an outer group marks the routes HTTPS but an inner one doesn't, the inner group gets ignored. The outer-most group with an HTTPS definition is the only one that counts.

Group Variable Regular Expressions

Groups support regular expressions for path variables:

$options = [
    'path' => '/users/:userId',
    'vars' => [
        'userId' => "\d+"
    ]
];
$router->group($options, function (Router $router) {
    $router->get('/profile', "MyApp\\ProfileController@showProfilePage");
    $router->get('/posts', "MyApp\\PostController@showPostsPage");
});

Going to /users/foo/profile or /users/foo/posts will not match because the Id was not numeric.

Note: If a route has a variable regular expression specified, it takes precedence over group regular expressions.

Caching

Routes must be parsed to generate the regular expressions used to match the host and path. This parsing takes a noticeable amount of time with a moderate number of routes. To make the parsing faster, Opulence caches the parsed routes. If you're using the skeleton project, you can enable or disable cache by editing config/http/routing.php.

Note: If you're in your production environment, you must run php apex framework:flushcache every time you add/modify/delete a route in config/http/routes.php.

Missing Routes

In the case that the router cannot find a route that matches the request, an Opulence\Http\HttpException will be thrown with a 404 status code.

URL Generators

A cool feature is the ability to generate URLs from named routes using Opulence\Routing\Urls\UrlGenerator. If your route has variables in the domain or path, you just pass them in UrlGenerator::createFromName(). Unless a host is specified in the route, an absolute path is generated. Secure routes with hosts specified will generate https:// absolute URLs.

Note: If you do not define all the non-optional variables in the host or domain, a UrlException will be thrown.

Generating URLs from Code

use Opulence\Routing\Urls\UrlGenerator;

// Let's assume the router and compiler are already instantiated
$urlGenerator = new UrlGenerator($router->getRoutes(), $compiler);
// Let's add a route named "profile"
$router->get('/users/:userId', "MyApp\\UserController@showProfile", ['name' => 'profile']);
// Now we can generate a URL and pass in data to it
echo $urlGenerator->createFromName('profile', 23); // "/users/23"

If we specify a host in our route, an absolute URL is generated. We can even define variables in the host:

// Let's assume the URL generator is already instantiated
// Let's add a route named "inbox"
$options = [
    'host' => ':country.mail.foo.com',
    'name' => 'inbox'
];
$router->get('/users/:userId', "MyApp\\InboxController@showInbox", $options);
// Any values passed in will first be used to define variables in the host
// Any leftover values will define the values in the path
echo $urlGenerator->createFromName('inbox', 'us', 2); // "http://us.mail.foo.com/users/2"

Generating URLs from Views

URLs can also be generated from views using the route() view function. Here's an example router config:

$router->get('/users/:userId/profile', 'UserController@showProfile', ['name' => 'profile']);

Here's how to generate a URL to the "profile" route:

<a href="{{! route('profile', 123) !}}">View Profile</a>

This will compile to:

<a href="/users/123/profile">View Profile</a>

Notes

Routes are matched based on the order they were added to the router. So, if you did the following:

$options = [
    'vars' => [
        'foo' => '.*'
    ]
];
$router->get('/:foo', "MyApp\\MyController@myMethod", $options);
$router->get('/users', "MyApp\\MyController@myMethod");

...The first route /:foo would always match first because it was added first. Add any "fall-through" routes after you've added the rest of your routes.