dzentota/router

Fast and flexible security aware router.

dev-master 2023-12-08 21:27 UTC

This package is auto-updated.

Last update: 2024-04-08 22:13:59 UTC


README

Usage

Usage of Router is as simple as:

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$route = (new Router())
    ->get('/user/{id}', 'UserController@show', ['id' => Id::class])
    ->findRoute($httpMethod, $uri);

The resolved $route will have the following structure (for GET /user/42):

array(4) {
  ["route"]=>
  string(10) "/user/{id}"
  ["method"]=>
  string(3) "get"
  ["action"]=>
  string(19) "UserController@show"
  ["params"]=>
  array(1) {
    ["id"]=>
    object(Id)#4 (1) {
      ["value":protected]=>
      string(2) "42"
    }
  }
}

Defining routes

The routes are added by calling addRoute() on the Router instance:

$r->addRoute($method, string $route, string $action, array $constraints = []);

The $method is an HTTP method string for which a certain route should match. It is possible to specify multiple valid methods using an array:

// These two calls
$r->addRoute('GET', '/test', 'handler');
$r->addRoute('POST', '/test', 'handler');
// Are equivalent to this one call
$r->addRoute(['GET', 'POST'], '/test', 'handler');

Router uses a syntax where {foo} specifies a placeholder with name foo. Every placeholder in the route must be typed. From security perspective, there is no sense in accepting data from the user (via HTTP) without properly validating it against your domain.

Assume we have a list of users stored in the database and these users may be retrieved by the ID that is a autoincrement positive integer. In such a case it's a good idea to introduce a domain primitive - ID:

<?php

use dzentota\TypedValue\Typed;
use dzentota\TypedValue\TypedValue;
use dzentota\TypedValue\ValidationResult;

class Id implements Typed
{
    use TypedValue;

    public static function validate($value): ValidationResult
    {
        $result = new ValidationResult();
        if (!is_numeric($value) || $value <= 0) {
            $result->addError('Bad ID');
        }
        return $result;
    }
}

Now you can use ID as a custom type for placeholder in the route

$r->get('/user/{id}', 'UserController@show', ['id' => Id::class])

Params of the route enclosed in {...?} are considered optional, so that /foo/{bar?} will match both /foo and /foo/bar.

Shortcut methods for common request methods

For the GET, POST, PUT, PATCH, DELETE, OPTIONS and HEAD request methods shortcut methods are available. For example:

$r->get('/get-route', 'get_handler');
$r->post('/post-route', 'post_handler');

Is equivalent to:

$r->addRoute('GET', '/get-route', 'get_handler');
$r->addRoute('POST', '/post-route', 'post_handler');

Also, there is a virtual ANY method that matches any request method, so:

$r->addRoute('ANY', '/route', 'get_handler');

Is equivalent to:

$r->addRoute(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], '/route', 'get_handler');

Route Groups

Additionally, you can specify routes inside a group. All routes defined inside a group will have a common prefix.

For example, defining your routes as:

$r->addGroup('/admin', function (Router $r) {
    $r->addRoute('GET', '/do-something', 'handler');
    $r->addRoute('GET', '/do-another-thing', 'handler');
    $r->addRoute('GET', '/do-something-else', 'handler');
});

Will have the same result as:

$r->addRoute('GET', '/admin/do-something', 'handler');
$r->addRoute('GET', '/admin/do-another-thing', 'handler');
$r->addRoute('GET', '/admin/do-something-else', 'handler');

Caching

You can dump and save routes using dump(), so later you can load them with load() Save routes:

file_put_contents('routes.php', sprintf('<?php return %s;',  var_export($r->dump(), true)));

Restore routes:

$routes = require 'routes.php';
$r->load($routes);

A Note on HEAD Requests

The HTTP spec requires servers to support both GET and HEAD methods:

The methods GET and HEAD MUST be supported by all general-purpose servers

To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an available GET route for a given resource. The PHP web SAPI transparently removes the entity body from HEAD responses so this behavior has no effect on the vast majority of users. Of course, you can always specify a custom handler for HEAD method