bramus/router
A lightweight and simple object oriented PHP Router. Built by Bram(us) Van Damme (https://www.bram.us) and Contributors
Features
- Static Route Patterns
- Dynamic Route Patterns
- Optional Route Subpatterns
- Supports
GET
,POST
,PUT
,DELETE
,OPTIONS
,PATCH
andHEAD
request methods - Supports
X-HTTP-Method-Override
header - Subrouting
- Allowance of
Class@Method
calls - Custom 404 handling
- Before Route Middlewares
- Before Router Middlewares
- After Router Middleware (Finish Callback)
- Works fine in subfolders
Prerequisites/Requirements
- PHP 5.3 or greater
- URL Rewriting
Installation
Installation is possible using Composer
composer require bramus/router ~1.3
Demo
A demo is included in the demo
subfolder. Serve it using your favorite web server, or using PHP 5.4+'s built-in server by executing php -S localhost:8080
on the shell. A .htaccess
for use with Apache is included.
Additionally a demo of a mutilingual router is also included. This can be found in the demo-multilang
subfolder and can be ran in the same manner as the normal demo.
Usage
Create an instance of \Bramus\Router\Router
, define some routes onto it, and run it.
// Require composer autoloader
require __DIR__ . '/vendor/autoload.php';
// Create Router instance
$router = new \Bramus\Router\Router();
// Define routes
// ...
// Run it!
$router->run();
Routing
Hook routes (a combination of one or more HTTP methods and a pattern) using $router->match(method(s), pattern, function)
:
$router->match('GET|POST', 'pattern', function() { … });
bramus/router
supports GET
, POST
, PUT
, DELETE
, and OPTIONS
HTTP request methods. Pass in a single request method, or multiple request methods separated by |
.
When a route matches, the attached route handling function will be executed. The route handling function must be a callable. Only the first route matched will be handled. When no matching route is found, an 'HTTP/1.1 404 Not Found'
status code will be returned.
Shorthands for single request methods are provided:
$router->get('pattern', function() { /* ... */ });
$router->post('pattern', function() { /* ... */ });
$router->put('pattern', function() { /* ... */ });
$router->delete('pattern', function() { /* ... */ });
$router->options('pattern', function() { /* ... */ });
$router->patch('pattern', function() { /* ... */ });
You can use this shorthand for a route that can be accessed using any method:
$router->all('pattern', function() { … });
Note: Routes must be hooked before $router->run();
is being called.
Route Patterns
Route patterns can be static or dynamic.
- Static Route Patterns are essentially URIs, e.g.
/about
. - Dynamic Route Patterns are Perl-compatible regular expressions (PCRE) that resemble URIs, e.g.
/movies/(\d+)
Commonly used subpatterns within Dynamic Route Patterns are:
\d+
= One or more digits (0-9)\w+
= One or more word characters (a-z 0-9 _)[a-z0-9_-]+
= One or more word characters (a-z 0-9 _) and the dash (-).*
= Any character (including/
), zero or more[^/]+
= Any character but/
, one or more
Note: The PHP PCRE Cheat Sheet might come in handy.
The subpatterns defined in Dynamic Route Patterns are converted to parameters which are passed into the route handling function. Prerequisite is that these subpatterns need to be defined as parenthesized subpatterns, which means that they should be wrapped between parens:
// Bad
$router->get('/hello/\w+', function($name) {
echo 'Hello ' . htmlentities($name);
});
// Good
$router->get('/hello/(\w+)', function($name) {
echo 'Hello ' . htmlentities($name);
});
Note: The leading /
at the very beginning of a route pattern is not mandatory, but is recommended.
When multiple subpatterns are defined, they resulting route handling parameters are passed into the route handling function in the order they are defined in:
$router->get('/movies/(\d+)/photos/(\d+)', function($movieId, $photoId) {
echo 'Movie #' . $movieId . ', photo #' . $photoId);
});
Optional Route Subpatterns
Route subpatterns can be made optional by making the subpatterns optional by adding a ?
after them. Think of blog URLs in the form of /blog(/year)(/month)(/day)(/slug)
:
$router->get(
'/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?',
function($year = null, $month = null, $day = null, $slug = null) {
if (!$year) { echo 'Blog overview'; return; }
if (!$month) { echo 'Blog year overview'; return; }
if (!$day) { echo 'Blog month overview'; return; }
if (!$slug) { echo 'Blog day overview'; return; }
echo 'Blogpost ' . htmlentities($slug) . ' detail';
}
);
The code snippet above responds to the URLs /blog
, /blog/year
, /blog/year/month
, /blog/year/month/day
, and /blog/year/month/day/slug
.
Note: With optional parameters it is important that the leading /
of the subpatterns is put inside the subpattern itself. Don't forget to set default values for the optional parameters.
The code snipped above unfortunately also responds to URLs like /blog/foo
and states that the overview needs to be shown - which is incorrect. Optional subpatterns can be made successive by extending the parenthesized subpatterns so that they contain the other optional subpatterns: The pattern should resemble /blog(/year(/month(/day(/slug))))
instead of the previous /blog(/year)(/month)(/day)(/slug)
:
$router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) {
// ...
}
Note: It is highly recommended to always define successive optional parameters.
To make things complete use quantifiers to require the correct amount of numbers in the URL:
$router->get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) {
// ...
}
Subrouting / Mounting Routes
Use $router->mount($baseroute, $fn)
to mount a collection of routes onto a subroute pattern. The subroute pattern is prefixed onto all following routes defined in the scope. e.g. Mounting a callback $fn
onto /movies
will prefix /movies
onto all following routes.
$router->mount('/movies', function() use ($router) {
// will result in '/movies/'
$router->get('/', function() {
echo 'movies overview';
});
// will result in '/movies/id'
$router->get('/(\d+)', function($id) {
echo 'movie id ' . htmlentities($id);
});
});
Nesting of subroutes is possible, just define a second $router->mount()
in the callable that's already contained within a preceding $router->mount()
.
Class@Method
calls
We can route to the class action like so:
$router->get('/(\d+)', '\App\Controllers\User@showProfile');
When a request matches the specified route URI, the showProfile
method on the User
class will be executed. The defined route parameters will be passed to the class method.
If most/all of your handling classes are in one and the same namespace, you can set the default namespace to use on your router instance via setNamespace()
$router->setNamespace('\App\Controllers');
$router->get('/users/(\d+)', 'User@showProfile');
$router->get('/cars/(\d+)', 'Car@showProfile');
Custom 404
Override the default 404 handler using $router->set404(function);
$router->set404(function() {
header('HTTP/1.1 404 Not Found');
// ... do something special here
});
Or using $router->set404('Class@Method');
$router->set404('\App\Controllers\Error@notFound');
The 404 will be executed when no route pattern was matched to the current URL.
Before Route Middlewares
bramus/router
supports Before Route Middlewares, which are executed before the route handling is processed.
Like route handling functions, you hook a handling function to a combination of one or more HTTP request methods and a specific route pattern.
$router->before('GET|POST', '/admin/.*', function() {
if (!isset($_SESSION['user'])) {
header('location: /auth/login');
exit();
}
});
Unlike route handling functions, more than one before route middleware is executed when more than one route match is found.
Before Router Middlewares
Before route middlewares are route specific. Using a general route pattern (viz. all URLs), they can become Before Router Middlewares (in other projects sometimes referred to as before app middlewares) which are always executed, no matter what the requested URL is.
$router->before('GET', '/.*', function() {
// ... this will always be executed
});
After Router Middleware / Run Callback
Run one (1) middleware function, name the After Router Middleware (in other projects sometimes referred to as after app middlewares) after the routing was processed. Just pass it along the $router->run()
function. The run callback is route independent.
$router->run(function() { … });
Note: If the route handling function has exit()
ed the run callback won't be run.
Overriding the request method
Use X-HTTP-Method-Override
to override the HTTP Request Method. Only works when the original Request Method is POST
. Allowed values for X-HTTP-Method-Override
are PUT
, DELETE
, or PATCH
.
Integration with other libraries
Integrate other libraries with bramus/router
by making good use of the use
keyword to pass dependencies into the handling functions.
$tpl = new \Acme\Template\Template();
$router->get('/', function() use ($tpl) {
$tpl->load('home.tpl');
$tpl->setdata(array(
'name' => 'Bramus!'
));
});
$router->run(function() use ($tpl) {
$tpl->display();
});
Given this structure it is still possible to manipulate the output from within the After Router Middleware
A note on working with PUT
There's no such thing as $_PUT
in PHP. One must fake it:
$router->put('/movies/(\d+)', function($id) {
// Fake $_PUT
$_PUT = array();
parse_str(file_get_contents('php://input'), $_PUT);
// ...
});
A note on making HEAD requests
When making HEAD
requests all output will be buffered to prevent any content trickling into the response body, as defined in RFC2616 (Hypertext Transfer Protocol -- HTTP/1.1): The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request.
Unit Testing & Code Coverage
bramus/router
ships with unit tests using PHPUnit.
-
If PHPUnit is installed globally run
phpunit
to run the tests. -
If PHPUnit is not installed globally, install it locally throuh composer by running
composer install --dev
. Run the tests themselves by callingvendor/bin/phpunit
.The included
composer.json
will also installphp-code-coverage
which allows one to generate a Code Coverage Report. Runphpunit --coverage-html ./tests-report
(XDebug required), a report will be placed into thetests-report
subfolder.
Acknowledgements
bramus/router
is inspired upon Klein, Ham, and JREAM/route . Whilst Klein provides lots of features it is not object oriented. Whilst Ham is Object Oriented, it's bad at separation of concerns as it also provides templating within the routing class. Whilst JREAM/route is a good starting point it is limited in what it does (only GET routes for example).
License
bramus/router
is released under the MIT public license. See the enclosed LICENSE
for details.