Routing
Table of Contents
- Introduction
- Basic Usage
- Route Variables
- Host Matching
- Middleware
- HTTPS
- Named Routes
- Route Grouping
- URL Generators
- Caching
- Missing Routes
- 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:
- The path the route is valid for
- The HTTP method (eg "GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", or "HEAD") the route is valid for
- The action to perform on a match
Opulence\Routing\Router
supports various methods out of the gate:
delete()
get()
head()
options()
patch()
post()
put()
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.