An blazing fast PHP router system with amazing features and abstraction.
Thank's to Nikita Popov's for motivate me with this post.
Add codeburner/router
to your composer.json
file, and update or install composer dependencies.
{
"require": {
"codeburner/router": "^2.0"
}
}
or via CLI:
$ composer require codeburner/router --save
Welcome to the fastest PHP router system docs! Before starting the usage is recommended understand the main goal and mission of all parts of this package.
Codeburner project create packages with performance in focus, the Codeburner Router was compared with Nikic's fast route a fast and base package for several route systems, including the Laravel and SlimFramework.
The Tests reveals that Codeburner Router can be in average 70% faster while give a full abstraction level in handling routes. More details about the benchmark including the comparisons using blackfire of scripts that maps 100 routes with several arguments and execute them, can be found here.
The router recognize requests and maps to a logic, an action. For example, when the application receive an incoming request for:
"GET" "/article/17"
It asks the router to match it to a action, if the first matching route is:
$collector->get("/article/{id}", "ArticleResource::show");
The request is dispatched to the ArticleResource
's show
method with 17
as parameter.
After a successful installation via composer, or a manual including of Collector.php
and Matcher.php
you can begin using the package.
include "vendor/autoload.php";
use Codeburner\Router\Collector;
use Codeburner\Router\Matcher;
$collector = new Collector();
$matcher = new Matcher($collector);
$collector->get("/", function () {
echo "Hello World!";
});
$route = $matcher->match("get", "/");
$route->call();
More examples can be found here.
After the release of v2.0.0 all routes are objects, and can be handled in groups. All route attributes can be modified at run time, and you can store a route created in the Codeburner\Router\Collector
in a var. Every time you create a route by any Codeburner\Router\Collector
method, a new Route
object is created and by default a Group
is returned containing these route.
Patterns are representation of request paths, it follows the popular definitions created by FastRoute, if you are familiar with Laravel or Slim Framework you will not have problems here.
If you not, all you need to know for now, is that dynamic segments in patterns, or vars or even parameters if you prefer, are defined inside {
and }
. All parts of pattern inside this will be captured by the matcher and passed to the action.
Routes can define dynamic pattern segments, and that can have a constraint of match, in other words you could define that these segment must be an int
or a uid
. The constraint definition follows the pattern adopted by most of routers, for example, to enforce the format of slugs the constraint must be something that have letters, numbers and hyphens, not more.
"/articles/{article:[a-z0-9-]+}"
NOTE: As a constraint is essentially a portion of a bigger REGEX there is a restriction of use of capturing groups. For example
{lang:(en|de)}
is not a valid placeholder, because()
is a capturing group. Instead you can use either{lang:en|de}
or{lang:(?:en|de)}
.
THe collector came with support to wildcards in place of regexes in constraints, you can define your own wildcards with the setWildcard(string name, string regex)
collector's method. There are 8 wildcards with more 3 aliases in a total of 11 wildcards, that are listed bellow:
- uid:
uid-[a-zA-Z0-9]
- slug:
[a-z0-9-]
- string:
\w
- int or integer:
\d
- float or double:
[-+]?\d*?[.]?\d
- hex:
0[xX][0-9a-fA-F]
- octal:
0[1-7][0-7]
- bool or boolean:
1|0|true|false|yes|no
NOTE: All the build-in wildcards came with no quantifier, but support quantifiers after they use, it's not a rule.
You can define several patterns at once, with optional segments that can be nested. For optinal segments in your routes use the [
and ]
statement to embrace the optional part. Optional segments must only be in the end of pattern and close all opened [
with ]
. For example:
"/user/photos[/{id:uid}]"
Routes define what request must execute what action. An action support all the definitions ways of a callables of PHP.
All the parameters defined on the route pattern are accessible to be used on a dynamic action definition. All parameters will be snake-cased, and words separator are identified by a "-" character. In the example bellow if we request /photos/get/30
the PhotosResource
's getLimited
method will be called with 30
as parameter.
"/{resource:string+}/get/{count:int+}" "{resource}Controller::getLimited"
A route must be able to execute the action. By default the action is executed by a simple call_user_func_array
call, but you can define a more specific way to do that individually for each route or group with the setStrategy
method, so each route can have different behaviors.
To define a new strategy simply create a class that implements the Codeburner\Router\Strategies\StrategyInterface
interface and the call
method that receives the Codeburner\Router\Route
matched as unique parameter. A strategy can receive the Codeburner\Router\Matcher
using the interface Codeburner\Router\Strategy\MatcherAwareInterface
.
The route enhancer strategy act like a bridge between one route and it dispatch strategy. In this "bridge" operations are made manipulating the route object to adapt it, it's common to use the metadata information in this place.
The real strategy is defined in the route metadata, using the key strategy
, after the execution of enhancement logic the real strategy will be called.
To create one enhancer you only need to extends the Codeburner\Router\Strategies\EnhancerAbstractStrategy
.
Codeburner have support to psr7 objects, there are at this version two strategies that give your actions a request
and response
objects and handle generated response
objects. Both Codeburner\Router\Strategies\RequestResponseStrategy
and Codeburner\Router\Strategies\RequestJsonStrategy
receive one Psr\Http\Message\RequestInterface
and one Psr\Http\Message\ResponseInterface
, and make the matcher
's call
method return a Psr\Http\Message\ResponseInterface
.
$collector->get("/article/{id:int{5}}", function (RequestInterface $request, ResponseInterface $response, array $args) {
return (string) getCommentById($args["id"]);
})->setStrategy(new RequestResponseStrategy($request, $response));
$collector->get("/article/{id:int{5}}", function (RequestInterface $request, array $args) {
return (array) getCommentById($args["id"]);
})->setStrategy(new RequestJsonStrategy($request, $response));
Instead of creating strategy objects by yourself, you could use a container wrapper on call
method.
$route = $collector->get("/{id:int{5}}", function (RequestInterface $request, ResponseInterface $response, array $args) {
return $args["id"];
});
$route->call(function ($class) use ($container) {
return $container->get($class);
});
If is necessary to define arguments that are not found on the pattern, use the setDefault(string key, mixed value)
method. These arguments will be merged with the given from the pattern.
$collector->get("/", function ($arg) {
echo $arg;
})->setDefault("arg", "hello world");
If is necessary to inject dependencies on controllers, resources, or even strategies, you can tell the call
method on Route
objects to use a closure that receives the class name and return one instance of these class.
$route->call(function ($class) use ($container) {
return $container->get($class);
});
All the routes allow you to apply names to then, this names can be used to find a route in the Collector
or to generate links with Path
's method to(string name, array args = [])
. E.g.
// ...
// The Path class will create several links for us, just give they new object a instance of the collector.
$path = new Codeburner\Router\Path($collector);
// ...
// Setting the name of route to blog.article
$collector->get("/blog/{article:slug+}", "blog::show")->setName("blog.article");
// ...
// this will print an anchor tag with "/blog/my-first-article" in href.
echo "<a href='", $path->to("blog.article", ["article" => "my-first-article"]), "'>My First Article</a>";
NOTE: For best practice use the dot for delimiting namespaces in your route names, so you can group and find they names easily. The resource collector adopt this concept.
Sometimes you want to delegate more information to a route, for post match filters or action execution strategies. For persist data that will not be passed to action but used in somewhere before the execution use the setMetadata(string key, mixed value)
method.
For getting the metadata use the Codeburner\Router\Route
's getMetadataArray()
method for getting all of each, and getMetadata(string key)
to get a specific metadata, you can check if a metadata exists with hasMetadata(string key)
method.
The collector hold all routes and give to the matcher, and more important than that, implements all the abstraction layer of defining routes.
All routes returned by the collector are Codeburner\Router\Group
instances, even if it's a single route. With these groups you can use most of the Codeburner\Router\Route
methods but applying the changes to all routes in the group. You can create groups with the Codeburner\Router\Collector
's group
method that receive an array of routes or instantiating a new instance of Codeburner\Router\Group
and add all routes with set
method.
Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index, show, make, edit, create, update and destroy actions, a resourceful route declares them in a single line of code.
$collector->resource('PhotosResource');
The collector will create seven new routes for PhotosResource
, as listed bellow:
Method | Path | Controller::Action | Used For |
---|---|---|---|
GET | /photos | PhotosResource::index | Display a list of all photos |
GET | /photos/make | PhotosResource::make | Return an HTML form for creating a new photo |
POST | /photos | PhotosResource::create | Create a new photo |
GET | /photos/{id} | PhotosResource::show | Display a specific photo |
GET | /photos/{id}/edit | PhotosResource::edit | Return an HTML form for editing a photo |
PUT | /photos/{id} | PhotosResource::update | Update a specific photo |
DELETE | /photos/{id} | PhotosResource::destroy | Delete a specific photo |
NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions.
There is two ways to define what of the seven resource routes should be created, with the only
or except
as option in resource(string resource, array options = null)
method,
// create only the index and show routes.
$collector->resource("ArticleResource", ["only" => ["index", "show"]]);
// create only the index and show routes too, because all the others should not be created.
$collector->resource("ArticleResource", ["except" => ["make", "create", "destroy", "update", "edit"]]);
or with the only
and except
methods of Codeburner\Router\Resource
object returned by the resource(string resource, array options = null)
method.
// create only the index and show routes.
$collector->resource("ArticleResource")->only(["index", "show"]);
// create only the index and show routes too, because all the others should not be created.
$collector->resource("ArticleResource")->except["make", "create", "destroy", "update", "edit"]);
By default all resource patterns receive the resource name as prefix, on previous example the UserResource
generate a /user
prefix. To alter this pass an array with as
option to the resource(string resource, array option = null)
method, these option will be used as prefix. eg.
// now the pattern for make action will be /account/make
$collector->resource("UserResource", ["as" => "account"]);
You can avoid this by using the resourceWithoutPrefix(string resource)
instead of resource(string resource, array option = null)
method, the same way for multiple matching methods resources(string[] resource)
and resourcesWithoutPrefix(string[] resources)
.
It's common to have resources that are logically children of other resources. For example one article
always have one category
. Nested routes allow you to capture this relationship in your routing. In this case, you could include this route declaration:
$collector->resource("CategoryResource")->nest(
$collector->resource("ArticleResource")
);
In addition to the routes for CategoryResource
, this declaration will also route to ArticleResource
with one category as parameter.
Method | Path | Controller::Action | Used For |
---|---|---|---|
GET | /category/{category_id}/article | ArticleResource::index | Display a list of all Article |
GET | /category/{category_id}/article/make | ArticleResource::make | Return an HTML form for creating a new article |
POST | /category/{category_id}/article | ArticleResource::create | Create a new article |
GET | /category/{category_id}/article/{id} | ArticleResource::show | Display a specific article |
GET | /category/{category_id}/article/{id}/edit | ArticleResource::edit | Return an HTML form for editing a article |
PUT | /category/{category_id}/article/{id} | ArticleResource::update | Update a specific article |
DELETE | /category/{category_id}/article/{id} | ArticleResource::destroy | Delete a specific article |
You can nest resources within other nested resources if you like. For example:
$collector->resource("CategoryResource")->nest(
$collector->resource("ArticleResource")->nest(
$collector->resource("CommentResource")
)
);
Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize paths such as:
"/category/1/article/2/comment/3"
TIP: Resources should never be nested more than 1 level deep.
One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this:
$collector->resource("ArticleResource")->nest(
$collector->resource("CommentResource")->only(["index", "make", "create"]);
);
$collector->resource("CommentResource")->except(["index", "make", "create"]);
This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the shallow
method in Codeburner\Router\Resource
:
$collector->resource("ArticleResource")->shallow(
$collector->resource("CommentResource")
);
This will generate the exact same routes as the first example.
NOTE:
shallow
method act the same way asnest
method, so you can always nest these methods, and use one with each other.
You are not limited to the seven routes that RESTFul routing creates by default. If you like, you may add additional routes that apply to the Codeburner\Router\Resource
. The example above will create an additional route with /photos/{id}/preview
pattern in get
method.
$collector->resource("PhotosResource")->member(
$collector->get("/preview", "PhotosResource::preview")
);
All the routes in resource receive a name that will be composed by the resource name or prefix, a dot and the action name. e.g.
class PhotosResource {
public function index() {
}
}
$collector->resource("PhotosResource")->only("index");
$collector->resource("PhotosResource", ["as" => "picture"])->only("index");
echo $path->to("photos.index"), "<br>", $path->to("picture.index");
If you prefer to translate the patterns generated by the resource, just define an translate
option that receives an array with one or the two keys, new
and edit
.
$collector->resource("ArticleResource", ["as" => "kategorien", "translate" => ["new" => "neu", "edit": "bearbeiten"]);
Or using the translate(array translations)
method of Codeburner\Router\Resource
.
$collector->resource("ArticleResource", ["as" => "kategorien"])->translate(["new" => "neu", "edit": "bearbeiten"]);
The two examples above translate ArticleResource
routes to german, changing the prefix to kategorien
and the new
and edit
keywords to neu
and bearbeiten
respectively.
Controllers can be fully mapped by the Codeburner\Router\Collector
, avoiding the manually description of routes to controller actions. To reach this abstraction some definitions must be respected:
- Methods that can be matched must begin with the corresponding HTTP method, like
get
,post
,put
,patch
anddelete
. - Camelcased method name will be converted to pattern, each word by default will receive
/
by prefix.
class UserController
{
public function getName()
{
// the same as $collector->get("/user/name", "UserController::getName")
}
}
All the PHPDoc @param are parsed and the methods arguments receive a constraint. All the wildcards are allowed here, and you can set the type of argument as an constraint too.
A new annotation is available for defining strategies to specific methods. For this use the @strategy
annotation.
class BlogController
{
/**
* @param int $id
* @annotation MyActionExcecutorStrategy
*/
public function getPost($id)
{
// the same as $collector->get("/blog/post/{id:int+}", "BlogController::getPost")
// ->setStrategy("MyActionExecutorStrategy")
}
}
Act the same way of prefixing resources, passing the option as
to controller(string controller, array options = null)
method.
Same way of ignoring resource name, use the controllerWithoutPrefix(string controller)
method, or the controllersWithoutPrefix(string[] controllers)
method.
If you wanna change the default pattern joiner /
by another join like -
, you only need to define that before the call of Codeburner\Router\Collector
's controller
method.
In the example bellow the pattern constructed by the getName
method of UserController
will be /user-name
instead of /user/name
.
$collector->setControllerActionJoin("-");
$collector->controller("UserController");
The matcher is responsible for determining which route should be executed for a given request information.
An important point of matcher is that it can remove the basepath prefix from the routes patterns, for this the first parameter of the matcher constructor should be a string with the basepath.
So if you want to declare routes for a blog system living in https://www.yourdomain.com/blog
create a new matcher that ignore the /blog
, so all you declarations can skip this segment.
There are several exceptions for HTTP errors provided by the codeburner router system, but there are special exceptions, that have methods for determining the logic of failure.
Route method is wrong Codeburner\Router\Exceptions\Http\MethodNotAllowedException
$collector->get("/foo", "controller::action");
try {
$matcher->match("post", "/foo");
} catch (Codeburner\Router\Exceptions\MethodNotAllowedException $e) {
// You can for example, redirect to the correct request.
// this if verify if the requested route can serve get requests.
if ($e->can("get")) {
// if so, dispatch into get method.
$matcher->match("get", $e->requested_uri);
}
}
- Codeburner\Router\Exceptions\BadRouteException
- Codeburner\Router\Exceptions\MethodNotSupportedException
- Codeburner\Router\Exceptions\Http\HttpExceptionAbstract
- Codeburner\Router\Exceptions\Http\BadRequestException
- Codeburner\Router\Exceptions\Http\ConflictException
- Codeburner\Router\Exceptions\Http\ForbiddenException
- Codeburner\Router\Exceptions\Http\GoneException
- Codeburner\Router\Exceptions\Http\LengthRequiredException
- Codeburner\Router\Exceptions\Http\MethodNotAllowedException
- Codeburner\Router\Exceptions\Http\NotAcceptableException
- Codeburner\Router\Exceptions\Http\NotFoundException
- Codeburner\Router\Exceptions\Http\PaymentRequiredException
- Codeburner\Router\Exceptions\Http\PreconditionFailedException
- Codeburner\Router\Exceptions\Http\RequestTimeOutException
- Codeburner\Router\Exceptions\Http\ServiceUnavailableException
- Codeburner\Router\Exceptions\Http\UnauthorizedException
- Codeburner\Router\Exceptions\Http\UnsupportedMediaTypeException
NOTE: The HTTP specification requires that a
405 Method Not Allowed
response include theAllow:
header to detail available methods for the requested resource. For this you can get a string with a processed allowed methods by using theallowed
method of this exception.