monomelodies / reroute
Extremely flexible PHP5 router
Requires
- jakeasmith/http_build_url: ^1.0.0
- league/pipeline: ^0.1.0
- zendframework/zend-diactoros: ^1.2
Requires (Dev)
- phpunit/phpunit: 4.0.*
- dev-master
- 3.4.7
- 3.4.6
- 3.4.5
- 3.4.4
- 3.4.3
- 3.4.2
- 3.4.1
- 3.4.0
- 3.3.4
- 3.3.3
- 3.3.2
- 3.3.1
- 3.3.0
- 3.2.2
- 3.2.1
- 3.2.0
- 3.1.4
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.0
- 1.1.8
- 1.1.7
- 1.1.6
- 1.1.5
- 1.1.4
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.0
- 0.1.9
- 0.1.8
- 0.1.7
- 0.1.6
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- 0.0.4
- 0.0.3
- 0.0.2
- 0.0.1
- dev-develop
This package is auto-updated.
Last update: 2022-02-01 12:43:45 UTC
README
Flexible PHP5 HTTP router, with support for various types of URL matching, URL arguments, custom state handling and URL generation. Reroute is designed to be usable in any type of project or framework.
Installation
Composer (recommended)
composer require monomelodies/reroute
Manual installation
- Get the code;
- Clone the repository, e.g. from GitHub;
- Download the ZIP (e.g. from Github) and extract.
- Make your project recognize Reroute:
- Register
/path/to/reroute/src
for the namespaceReroute\\
in your PSR-4 autoloader (recommended); - Alternatively, manually
include
the files you need.
- Register
Basic Usage
when
? then
!
Since the Reroute router responds to HTTP requests, we use the when
and then
methods to respond:
<?php $router = new Router; $router->when('/some/url/')->then(function () { // Return something. });
when
starts matching whenever it can, so if your project lives under (for
example) http://my-url.com/bla/my-framework/libs
the example route above could
match /bla/my-framework/libs/some/url/
if nothing better was defined.
Note that Reroute matches parts of URLs, hence the fact that your defined route starts with
/
doesn't have any special meaning.
when
returns a new Router with the specified URL as its "base" (the first
constructor argument). For nested routers (see below), this includes the base
for all parent routers. Schematically:
<?php $router = new Router; $foo = $router->when('/foo/'); $bar = $foo->when('/bar/'); $baz = $bar->when('/baz/')->then('I match /foo/bar/baz/!');
What then
returns can be really anything. If you pass a callable, that in turn
should eventually return something non-callable. Hence, the following four forms
are equivalent:
<?php $router->when('/some/url/')->then(function () { return 'Hello world!'; });; $router->when('/some/url/')->then('Hello world!'); class Foo { public static function getInstance() { return new Foo; } public function __invoke() { return 'Hello world!'; } } $router->when('/some/url/')->then(new Foo); $router->when('/some/url/')->then(['Foo', 'getInstance']);
Named states
When called with two parameters, the first parameter is assumed to be the
(preferably unique) name of the state. Named states can be retrieved at any
point by calling get('name_of_state')
on the router:
<?php
$router->when('/the/url/')->then('myname', 'handler');
$state = $router->get('myname'); // Ok!
$state instanceof Reroute\State; // true
Resolving a request
After routes are defined, somewhere in your front controller you'll want to actually resolve the request:
<?php use Zend\Diactoros\ServerRequestFactory; if ($state = $router(ServerRequestFactory::fromGlobals())) { echo $state; } else { // 404! }
(Note that you don't need to explicitly pass in a ServerRequest
object, the
router uses the current request by default.)
Invoking the router starts a pipeline.
By calling the router's pipe
method you can add middleware to the stack.
If a valid state was found for the current URL, it's return value is returned by
the pipeline. Otherwise, it will resolve to null
.
To emulate a different request type than the actual one, simply change
$_SERVER['REQUEST_METHOD']
.
Passing parameters
Your URLs are actually regexes, so you can match variables to pass into the callback:
<?php $router->when("/(?'name'\w+)/")->then(function ($name) { return "Hi there, $name!"; });
Variables can be named (in which case the order you pass them to your callback doesn't matter - Reroute does reflection on the callable to determine the best fit) or anonymous (in which case they'll be passed in order).
Shorthand placeholders
For simpler URLs, you can also use a few shorthand placeholders. The following three statements are identical:
<?php $router->when("/(?'param'.*?)/"); $router->when('/:param/'); $router->when('/{param}/');
When using placeholders, note that one has less control over parameter types.
Using regexes is more powerfull since you can force e.g. "/(?'id'\d+)/"
to
match and integer. PHP 7 supports extended type hinting in callables, so this
will be improved in a future release.
Inspecting the current request
By type hinting a parameter as an instance of
Psr\Http\Message\RequestInterface
, you can inject the original request object
and check the used method (or anything else of course):
<?php use Psr\Http\Message\RequestInterface; $router->when('/some/url/')->then(function (RequestInterface $request) { switch ($request->getMethod()) { case 'POST': // Perform some action case 'GET': return 'ok'; default: return $request->getMethod()." method not allowed."; } });
Limiting to verbs (or extending the palet)
The default behaviour is to match GET
and POST
actions only since they are
most common in web applications. Normally a POST
to a static page should act
like a GET
. However, one can specifically instruct certain URLs to respond to
certain methods:
<?php use Zend\Diactoros\Response\EmptyResponse; $router->when('/some/url/')->then('my-awesome-state', function () { // Get not allowed! return new EmptyResponse(403); })->post(function () { // ...do something, POST is allowed... // Since we disabled get, this should redirect somewhere valid afterwards. });
Available verb methods are post
, put
, delete
, head
and options
.
Subsequent calls extend the current state, and any existing actions are
overridden on re-declaration.
Referring to other callbacks
A parameter typehinted as callable
matching a defined action (in uppercase)
can be used to "chain" to another action. So the following pattern is common for
URLs requiring special handling on e.g. a POST
:
<?php $router->when('/some/url/')->then('my-state', function() { return 'This is a normal page'; })->post(function (callable $GET) { // Perform some action... return $GET; });
Note there is no need to re-pass any URL parameters to the callable; they are
injected automatically. Hence, calls to get
and post
etc. may
accept/recognize different parameters in different orders.
Custom verb callbacks do not "bubble up" the routing chain. Hence, specifically disabling
POST
on/foo/
does not affect the default behaviour for/foo/bar/
.
If the injected action is not available for this state, a 405 error is returned instead.
Grouping
The optional second argument to when
is a callable, which expects a single
parameter: a new (sub) router. All routes defined using when
on the subrouter
will inherit the parent router's URL:
<?php $router->when('/foo/', function ($router) { $router->then('I match /foo/!'); $router->when('/bar/')->then('I match /foo/bar/!'); });
Since when
also returns the new subrouter, you can also use one of the
following patterns if you prefer:
<?php $router->when('/foo/')->when('/bar/')->then('I match /foo/bar/!'); // ...or... $foo = $router->when('/foo/'); $foo->when('/bar/')->then('I match /foo/bar/!');
For convenient chaining, then
returns the (sub)router itself:
<?php $router->when('/foo/') ->then('I match /foo/!') ->when('/bar/') ->then('But I match /foo/bar/!');
Pipelining middleware
Since routes are pipelined, you can at any point add one or more calls to the
pipe
method to add middleware:
<?php $router->when('/restricted/') ->pipe(function ($payload) { if (!user_is_authenticated()) { // In the real world, probably raise an exception you can // catch elsewhere and show a login page or something... return null; } return $payload; }) ->when('/super-secret-page/') ->then('For authenticated eyes only!');
You can call pipe
as often as you want. Subrouters won't be executed if the
pipeline is short-circuited anywhere.
When using named parameters, the pipelined callable can optionally specify which parameters it also wants to use:
<?php $router->when("/(?'foo':\d+)/") ->pipe(function ($payload, $foo) { if ($foo != 42) { // return error response or something... } return $payload; });
This is similar to the state resolving callable, except that there is always
a first parameter $payload
, and injecting the $request
isn't possible.
One common use of this is defining a pipe for a first $language
parameter in
a group of routes, and setting some environment variable to its value for all
underlying routes.
Generating URLs
To generate a URL for a defined named state, use the generate
method:
<?php $router->when('/:some/:params/')->then('myname', 'handler'); echo $router->generate('myname', ['some' => 'foo', 'params' => 'bar']); // outputs: /foo/bar/
The optional third parameter to generate is a boolean telling generate
if it
should prefer a route without scheme/host if the user is already on the current
host. It defaults to true. The above example might output
http://localhost/foo/bar/
if called with false
as the third parameter.
Generation is only possible for named states, since anonymous ones obviously could only be retrieved by their actual URL (in which case you might as well hardcode it...). Use named states if your URLs are likely to change over time!
Handling 404s and other errors
<?php $router->when(null)->then('404', function() { return "The URL did an oopsie!"; });
By passing null
as a URL, something random is generated interally that won't
normally match anything actual in the routing table. Hence, this is a safe
placeholder. But you could use anything, really, as long as it's not already in
use in your application.
Next, try to resolve the currently requested URI. On failure, use the 404 state instead:
<?php if ($state = $router()) { echo $state; } else { // Note that we must "invoke" the state. echo $router->get('404')(); }
A best practice is to wrap your state resolving in a try/catch
block, and
handle any error accordingly so views/controllers/etc. can throw exceptions.