This project provides a class library for routing requests to a PHP application. The library is designed to be small and minimal.
This library is available as a composer package. Require tccl/router
in your composer.json file and then install.
composer require tccl/router
Name | Description |
---|---|
TCCL\Router\Router |
Provides core routing functionality |
TCCL\Router\RequestHandler |
Provides interface for class-based request handlers |
TCCL\Router\RouterException |
Provides type for handling request handler exceptions |
Name | Description |
---|---|
TCCL\Router\RouterExceptionHandling |
Optional trait to add exception handling to a router subclass |
TCCL\Router\RouterMethodHandling |
Optional trait to add method handling support to a router subclass |
TCCL\Router\RouterRESTHandling |
Optional trait to add REST API support to a router subclass |
Router provides a mechanism for routing control to a handler based on an input URI. It is very easy to set up and use. Just create an instance of type TCCL\Router\Router
. The constructor takes two arguments:
- A handler used as the default when a route does not match
- The router base path (optional, defaults to none)
use TCCL\Router\Router;
function not_found(Router $router) {
$router->contentType = Router::CONTENT_TEXT;
$router->flush();
echo "Not found\n";
}
$router = new Router('not_found');
When you create a router, you may specify a base path as the second constructor argument. This is useful for when an application is running under a sub-directory of the document root. The router will automatically convert the request URIs relative to the configured base path when matching against the routing table.
You can also set the base path using the protected method
setBasePath
if you are subclassingTCCL\Router\Router
.
Pro Tip: To allow your application to work arbitrarily under any sub-directory of the document root, calculate the base path using the path of the directory containing the entry point script and the value of the DOCUMENT_ROOT
server variable.
// Given __FILE__:"/path/to/www/app/index.php"
$entryPointPath = dirname(__FILE__);
$documentRoot = $_SERVER['DOCUMENT_ROOT'];
$basePath = substr($entryPointPath,strlen($documentRoot));
// Given entryPointPath:"/path/to/www/app" and documentRoot:"/path/to/www",
// then we get basePath:"/app"
$router = new Router('not_found',$basePath);
This trick works if you have an entry point script (e.g. index.php
) that is called for each route; the entry point script must be installed at the root of the project tree.
Now you can add routes using the addRoute()
or addRoutesFromTable()
methods. Each route maps a request method and route specifier to a handler specifier.
// Method: Router::HTTP_GET (i.e. 'GET')
// Route Specifier: '/help'
// Handler Specifier: 'generate_help_page'
$router->addRoute(Router::HTTP_GET,'/help','generate_help_page');
$router->addRoute('GET','/\/help\/topics\/([0-9]+)/','generate_help_topic');
// The addRoutesFromTable() method allows multiple routes
// to be added from an array in one call.
$router->addRoutesFromTable([
Router::HTTP_GET => [
'/help' => 'generate_help_page',
'/\/help\/topics\/([0-9]+)/' => 'generate_help_topic',
],
]);
A route specifier indicates the URI that identifies a route. This can either be an exact match or a regex match.
$router->addRoute(Router::HTTP_GET,'/page/home','load_home_page');
// The following route specifier matches URIs such as:
// /page/1
// /page/27
// /page/33
// NOTE: forward-slashes in the URI spec must be properly escaped
// when using forward slashes to bracket the regex.
$router->addRoute(Router::HTTP_GET,'/\/page\/([0-9]+)/','load_page');
If the regex specified match groups, the matched values can be accessed via the Router::$matches
property during request handling.
A handler specifier identifies an executable context that can handle the route (i.e. the handler). The canonical implementation supports the following handler specifiers:
Specifier Type | Explanation |
---|---|
Callable | Anything that can be called as a function in PHP |
Class name | The name of a class that implements TCCL\Router\RequestHandler , or, for sub-routing, TCCL\Router\Router |
Object instance | An instance of a class that implements TCCL\Router\RequestHandler , or, for sub-routing, TCCL\Router\Router |
The simplest handler specifier is a callable. This can be a function or class method (static or non-static). Consult the PHP manual on callables for more.
Typically, you will use a handler that implements the TCCL\Router\RequestHandler
interface, as it provides more robust functionality. If you specify a class name, the router will create a new instance of the class if and when the route is executed. Otherwise, if you provide an instance, that instance will be used as-is when the route is executed.
When a route executes with a RequestHandler
, it will invoke the run()
method, passing the executing Router
as the parameter.
use TCCL\Router\Router;
use TCCL\Router\RequestHandler;
class PageHandler implements RequestHandler {
public static function not_found(Router $router) {
}
public function run(Router $router) {
$pageNumber = (int)$router->matches[1];
}
}
$router = new Router("PageHandler::not_found");
$router->addRoute(Router::HTTP_GET,'/\/page\/([0-9]+)/','PageHandler');
If the handler specifier is a class name or instance of type TCCL\Router\Router
, then the router will invoke a sub-router. Sub-routers work well for cases when you want to classify a set of routes under a common prefix. The route specifier for a sub-router should be a regex that matches a common prefix, and the Router::HTTP_ALL
special method specifier should be used to match any request method.
// Let 'APIRouter' process all URIs having a prefix of /api.
$router->addRoute(Router::HTTP_ALL,'/^\/api\//','APIRouter');
To actually route a request, you must execute the router using its route
method. You must specify the HTTP method and URI to route. These values may be obtained from _$SERVER
depending on your PHP SAPI.
$router->route($_SERVER['REQUEST_METHOD'],$_SERVER['REQUEST_URI']);
For non-trivial applications, subclassing the TCCL\Router\Router
class can better encapsulate routing functionality.
use TCCL\Router\Router;
use TCCL\Router\RouterException;
class MyRouter extends Router {
public function __construct() {
parent::__construct(function(){
throw new RouterException(
404,
'Not Found',
'The specified resource was not found on this server.'
);
});
$this->addRoutesFromTable([/* ... */]);
}
}
The library provides additional handling support to account for a number of common use cases including:
- Exception handling
- Method handling
- REST handling
Additional handling is implemented as traits that you import into a custom TCCL\Router\Router
subclass.
Router exception handling allows the router to handle exceptions in a convenient and streamlined manner. To add exception handling to a Router
subclass, use trait TCCL\Router\RouterExceptionHandling
.
You must also implement the handleServerError
and handleRouterError
methods to handle exceptions. The later method is for exceptions of type TCCL\Router\RouterException
and the former is for any other exception.
Router method handling allows you to define a single handler class with multiple methods that handle each request. To add method handling to a Router
subclass, use trait TCCL\Router\RouterMethodHandling
.
Once method handling has been added, you can add a method name to a class name handler specifier (e.g. Namespace\Class::methodName
). You do not need to implement RequestHandler
in the handler class when using method handling. Handler methods may be either static or non-static and receive a single Router
argument. For a static method, use the syntax @class::method
.
use TCCL\Router\Router;
use TCCL\Router\RouterException;
use TCCL\Router\RouterMethodHandling;
class MyRouter extends Router {
use RouterMethodHandling;
public function __construct() {
parent::__construct(function(){
throw new RouterException(
404,
'Not Found',
'The specified resource was not found on this server.');
});
$this->addRoute(Router::HTTP_GET,'/time','Handler::getTime');
$this->addRoute(Router::HTTP_GET,'/favnum','@Handler::getFavoriteNumber')
}
}
class Handler {
public function getTime(MyRouter $router) {
/* ... */
}
public static function getFavoriteNumber(MyRouter $router) {
/* ... */
}
}
REST handling adds additional functionality to a custom Router
subclass that makes writing REST API endpoints using JSON more convenient. The handling allows your request handler to return a payload that is then automatically converted into JSON. The correct HTTP headers are also applied.
If the handler returns null
, then HTTP 204
No Content is returned. The handler may return false
to prevent any handling of the return value; this is useful for when the handler needs to perform its own response generation.
To add REST handling to a Router
subclass, use trait TCCL\Router\RouterRESTHandling
.
Note: REST handling works well when paired with method handling.
use TCCL\Router\Router;
use TCCL\Router\RouterException;
use TCCL\Router\RouterMethodHandling;
use TCCL\Router\RouterRESTHandling;
class MyRouter extends Router {
use RouterMethodHandling;
use RouterRESTHandling;
public function __construct() {
parent::__construct(function(){
throw new RouterException(
404,
'Not Found',
'The specified resource was not found on this server.');
});
$this->addRoute(Router::HTTP_GET,'/time','Handler::getTime');
}
}
class Handler {
public function getTime(MyRouter $router) {
$dt = new \DateTime;
$repr = [
'year' => (int)$dt->format('Y'),
'month' => (int)$dt->format('m'),
'date' => (int)$dt->format('d'),
'hour' => (int)$dt->format('H'),
'minute' => (int)$dt->format('i'),
'second' => (int)$dt->format('s'),
'tz' => $dt->getTimezone()->getOffset(),
];
return $repr;
}
}
The library provides a mechanism for verifying a request payload that can avoid tedious boilerplate in the implementation of a request handler. Verification also helps to sanitize user input.
Payload verification functionality is defined in the TCCL\Router\PayloadVerify
class, but is primarily accessed via the TCCL\Router\Router::getPayloadVerify()
method.
Payload verification validates a request parameter payload using a format argument.
$format = [
'name' => 's',
'email' => 's',
];
$payload = $router->getPayloadVerify($format);
The payload is generated from the request parameters. This works for any type of HTTP request method.
Note: For HTTP
GET
requests, the data type for each parsed parameter will bestring
. You can apply promotions via payload verification, but you will want to make sure you are only type validating forstring
. For other request methods (e.g.POST
), the content type of the request payload can allow for other data types to be encoded.
If verification fails, then a TCCL\Router\PayloadVerifyException
is thrown. The PayloadVerifyException
class is a sub-type of a TCCL\Router\RouterException
having status code 400
. You can catch these exceptions and call printDebug()
to obtain diagnostic information about how the payload failed verification.
The payload verification functionality has a number of options that can be configured:
Option Name | Description | Default Value |
---|---|---|
checkExtraneous |
Determines if extraneous items in the payload cause verification to fail | true |
$format = [
'name' => 's',
'email' => 's',
];
$options = [
'checkExtranous' => false,
];
$payload = $router->getPayloadVerify($format,$options);
The verification format argument indicates the names and expected data types for the request parameters in the payload. The format can also indicate other actions to perform such as type promotions and constraint checks.
The format argument is an associative array mapping the parameter names to verification specifier values.
$format = [
'name' => [
'first' => 's',
'last' => 's',
],
'job_title' => 's',
'address_lines' => ['s'],
'age?' => 'i',
];
If a parameter name (e.g. age
) contains a trailing ?
character (e.g. age?
), then the parameter may be omitted (or null
) and the payload will still verify correctly. See the entry for age
in the above example.
If a scalar value is expected for a parameter, then the specifier is a string indicating the accepted types and any promotions/checks to apply. See the entry for job_title
in the above example.
If an indexed array of values is expected for a parameter, then the specifier is an indexed singleton array containing a scalar specifier. This specifier is applied to each element in the input value. See the entry for address_lines
in the above example.
If a nested associative array structure is expected for a parameter, then the specifier is a nested format array that is processed recursively. See the entry for name
in the above example.
A scalar format string tells the verification system to perform a number of actions to verify that a scalar value is correct. A format string has the following base structure:
<TYPE-SPECIFIER> [PROMOTION-SPECIFIER] [CHECK-SPECIFIER] ['?']
Type Specifiers: The type specifier portion of the string is the concatenation of one or more type specifier characters representing the set of types that are allowed. If the value does not match one of these types, then verification fails.
Type specifier characters are always lower-case letter or a symbol. The following type specifiers are provided in the current implementation:
Specifier Character | Type | Notes |
---|---|---|
b |
Boolean | |
s |
String | |
i |
Integer | |
f |
Float | |
d |
Double | Note that double is functionally equivalent to float |
Example type specifiers:
Specifier | Meaning |
---|---|
i |
Allow only integer |
si |
Allow string or integer |
Promotion Specifiers: The promotion specifier indicates an optional promotion operation to perform after the type validation. A promotion is generally an operation that converts the value; most of the core promotions provided by the current implementation are type promotions.
The promotion specifier portion of the format string is a single upper-case letter or symbol. The following promotions are provided in the current implementation:
Specifier Character | Meaning |
---|---|
B |
Convert to boolean |
S |
Convert to string |
I |
Convert to integer |
F |
Convert to float |
D |
Convert to double (alias of float) |
^ |
Trim string value (will promote to string) |
Example specifiers with promotion:
Specifier | Meaning |
---|---|
siS |
Allow string or integer and promote to string |
ibB |
Allow integer or boolean and promote to boolean |
s^ |
Allow string and trim |
Check Specifiers:
The check specifier performs one or more additional validation actions on the value after the promotion. Check actions are predicate actions that fail validation when they return false
.
The check specifier portion of the format string is the concatenation of one or more non-letter characters. The following check actions are provided in the current implementation:
Specifier Character | Meaning |
---|---|
! |
Ensures a string is not empty |
+ |
Ensures a numeric value is positive |
- |
Ensures a numeric value is negative |
* |
Ensures a numeric value is non-negative |
% |
Ensures a numeric value is non-zero |
Example specifiers with checks:
Specifier | Meaning |
---|---|
siI+ |
Ensures value is positive integer, accepts string representation of integer |
s^! |
Ensures trimmed string value is not empty |
Using the TCCL\Router\PayloadVerify
class, you can register custom type, promotion and check specifiers as described in the previous section. Example:
use TCCL\Router\PayloadVerify;
function is_user_id($value) : bool {
if (!is_numeric($value)) {
return false;
}
$id = (int)$value;
return $id >= 1;
}
PayloadVerify::registerType('u','is_user_id');
Note that when you register custom type, promotion or check specifiers, you must be careful to not override an existing specifier if you intend to keep using it. The PayloadVerify
class will validate the specifiers to ensure there are no conflicts. Specifiers must meet the requirements listed below:
Item | Requirements |
---|---|
Type Specifier | Must be lower-case letter a..z |
Promotion Specifier | Must be upper-case letter A..Z or a non-letter character; the set of non-letter promotion specifier characters must be mutually exclusive of the set of non-letter check specifier characters |
Check Specifier | Must be a non-letter character; the set of check specifier characters must be mutually exclusive of the set of non-letter promotion specifier characters |