A simple routing library
router - v4.6.1 -------------------------------------------------------------------------------- This repository provides a class library for routing requests to a PHP application. The library is designed to be small and minimal. Primary authors: Roger Gee <roger.gee@tulsalibrary.org> -------------------------------------------------------------------------------- Installation: This library is available as a composer package. Require 'tccl/router' in your composer.json file and then install. You must target at least v3.0.0 to use the composer package. -------------------------------------------------------------------------------- Interface: Classes TCCL\Router\Router The core routing object type TCCL\Router\RequestHandler An abstract interface for class-based request handlers TCCL\Router\RouterException An 'Exception' subclass for handling request handler errors Traits TCCL\Router\RouterExceptionHandling Optional trait to add to router subclass for exception handling 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 -------------------------------------------------------------------------------- Usage: Router provides a mechanism for routing control to a handler based on the input URI. It is very easy to setup and use. Just create an instance of type TCCL\Router\Router. The constructor takes a handler argument which is the default handler used when a route does not match: function not_found(\TCCL\Router\Router $router) { $router->contentType = \TCCL\Router\Router::CONTENT_TEXT; $router->flush(); echo "Not found\n"; } $router = \TCCL\Router\Router('not_found'); Now you can add routes using the addRoute() or addRoutesFromTable() methods. Each route maps a request method and URI to a handler description. A handler description identifies an executable context that can handle the route (i.e. the handler). The canonical Router implementation supports the following handler descriptions: * PHP callable * Class name of class that implements \TCCL\Router\RequestHandler or \TCCL\Router\Router * Object instance of class that implements \TCCL\Router\RequestHandler or \TCCL\Router\Router If a TCCL\Router\RequestHandler is specified, the Router will invoke its run() method. If a TCCL\Router\Router is specified, then the Router will pass handling to an instance of that Router. (This is a special case that allows you to define subrouters.) Route URIs may be exact literal routes or PHP regex strings: $router->addRoute('GET','/help','generate_help_page'); $router->addRoute('GET','/\/help\/topics\/([0-9]+)/','generate_help_topic'); Any matches found upon successfully matching a regex path are set in the router's 'matches' property: function generate_help_topic(\TCCL\Router\Router $router) { $node = $router->matches[1]; // get second match (first is entire route) // ... } Finally we need to route the request. You will have to specify the URI/method. This is easily obtained via the superglobals set by whatever PHP SAPI you are using. Optionally you can specify a base path for the route. This is useful when your application runs under a subdirectory (i.e. you won't have to change your routes). Here is a routing that assumes every route is relative to the document root. $router->route($_SERVER['REQUEST_METHOD'],$_SERVER['REQUEST_URI']); Here is routing that works under an arbitrary base path. This method assumes the file running the routing code is under the application route directory. Be careful if this file is a soft link referring to a file outside the application distribution (it won't work). $basePath = substr(dirname(__FILE__),strlen($_SERVER['DOCUMENT_ROOT'])); $router->route($_SERVER['REQUEST_METHOD'],$_SERVER['REQUEST_URI'],$basePath); A common idiom for using Routers is to subclass Router and add the routers in the constructor. You can also use the setBasePath() protected method to set your base path before the routing is performed. class AppRouter extends \TCCL\Router\Router { public function __construct() { parent::construct(function() { // Not Found handler }); // Add routes. $this->addRoute(...); // Set base path. $this->setBasePath(...); } } -------------------------------------------------------------------------------- Library Overview: [TCCL\Router\RequestHandler] run(Router $router) [TCCL\Router\Router] __construct($notFoundHandler) Creates a new instance with the specified fallback handler for non-existent routes addRoute($method,$uri,$handler) Adds a new route to the router; the handler is either a callable or a class name/object that implements the RequestHandler interface or extends the Router class. The $method may be an array of methods or \TCCL\Router\Router::HTTP_ALL for all request methods. Supported methods include 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD' and 'OPTIONS'. The $uri is either a literal string route or a regex identifying a set of routes to match. Any regex matches will be placed in $router->matches. The special route '/' identifies an exact match of the base path, which may or may not include a trailing '/'. If the handler is a Router subclass/instance, then your $uri should be a regex (and exception will be thrown if not). The global match will specify the new base path for the subrouter. This means your $uri should always specify the base path for the subrouter. For example: $router->addRoute(Router::HTTP_ALL,'/\/api/','APIRouter'); In this example, $router will create a subrouter of type APIRouter for any path under "/api" (including "/api"). All paths in APIRouter will be relative to "/api". addRoutesFromTable(array $table) Adds a set of routes from the specified table into the router. This allows you to import a bunch of routes at once. The format of the $table array is an associative array that maps a request method to a request URI pattern to a handler description. For example: $table = ['GET' => ['/foo' => 'MyHandler']] route($method,$uri,$basedir) Routes a request with the specified method and URI The base directory specifies the base directory for requests; if this value is non-empty, then the router will interpret URIs relative to the base directory; for example: in route table: "GET" "/A/B" ----> "handler1" route "GET" "/base/A/B" with base directory "/base": execute "handler1" since "/base/A/B" matched URI "/A/B" in route table addHeader($key,$value) Adds header pair to list of headers; header info must be written later with a call to flush() flush() Writes metadata to the output stream; this includes the response code, content-type and any headers redirect($uri,$params) Forces a redirect The URI will be evaluated relative to any configured base directory; as such this function SHOULD be called in a request handler after a call to route() Any parameters are encoded and appended to the URI as query parameters getURI($component,$params) Gets a URI The URI will have any configured base path prepended and any parameters appended as a query string getRequestType() Gets the request content type The SAPI must have provided this in $_SERVER['CONTENT_TYPE'] or $_SERVER['HTTP_CONTENT_TYPE'] getRequestParam($name,$default = null) Gets the named request parameter, returning $default if it is not found. getRequestParamVerify($name,$format,$default = null) Gets the named request parameter with applied verification. getPayloadVerify($format) Gets the full request payload with applied verification. [protected] createHandler($handler) May be overridden by custom routers to provide new route handler description formats. A derived implementation should always call down into the base implementation defined in Router. [protected] resultHandler($result) Handles the result of the route handler operation (i.e. the value returned by a handler). The canonical implementation does nothing with this value; however a derived implementation can override this method to handle a return value in some way. [protected] setBasePath($basedir) Sets the router base path. [TCCL\Router\RouterExceptionHandling] Use the RouterExceptionHandling trait to add simple exception handling to a custom router. For example: class MyRouter extends \TCCL\Router\Router { use \TCCL\Router\RouterExceptionHandling; protected function handleServerError(Exception $ex) { // ... } protected function handleRouterError(RouterException $ex) { // ... } } In a handler, you can throw a RouterException, assigning the exception object an HTTP status code and error message. You then can support the handling of multiple kinds of errors in one location in your codebase. [TCCL\Router\RouterMethodHandling] Use the RouterMethodHandler trait to add method handler support to a custom router. This allows you to use handler descriptions like the following: "ClassName::MethodName" For example: $router->addRoute('GET','/trinket','MyHandler::executeTrinket'); [TCCL\Router\RouterRESTHandling] Use the RouterRESTHandling trait to add support for writing JSON-based REST API routers. A router that absorbs this trait will automatically write JSON payloads that were returned from request handlers. If NULL is returned from a request handler, an HTTP 204 "No Content" message is generated. To prevent the functionality from writing to output, return FALSE. The trait also provides basic functionality for writing JSON objects and other common REST API responses. -------------------------------------------------------------------------------- Payload Verification: The library provides a simple mechanism for verifying a request payload. While the functionality is designed to be used indirectly through the TCCL\Router\Router class, the raw implementation can be accessed via the TCCL\Router\PayloadVerify class. Payload verification is denoted via a format parameter. A format parameter is a string or array that denotes the structure of the request payload (or a part of it when verifying only a single parameter). If the format is an array, it may be either a dictionary or a sequence. Additionally, arrays can denote nested format parameter structures. When a payload fails verification, a RouterException is thrown with error code HTTP 400. The simplest format parameter is a scalar format parameter. For example, the following format parameter matches an integer: 'i' A scalar format parameter is a string resulting from the concatenation of two string components. The first component of the format string denotes the set of types that are accepted by the format. Types are denoted using a lowercase character, and one or more characters may be provided. The second component of the format string denotes the promotion (if any). Promotions are denoted using an uppercase letter. When a promotion is specified, the payload is modified to contain the promoted value; the promotion component can be omitted to leave the value untouched. Structure: '<types>[PROMOTION]' Example: 'isS' (accept a string or integer and promote to string) The following types and promotions are supported by default: TYPES PROMOTIONS 's' - String 'S' - Promote to string 'i' - Integer 'I' - Promote to integer 'f' - Float 'F' - Promote to float 'd' - Double 'D' - Promote to double 'b' - Boolean 'B' - Promote to boolean You may define custom types/promotions using the PayloadVerify::registerType() and PayloadVerify::registerPromotion() methods. This allows you to perform more advanced payload validation. A scalar format parameter may be suffixed with '?' to allow null. Example: 'siS?' - Accepts a string or integer, promotes to string, accepts null If the format parameter is an array, then an array structure is verified for the payload. There are two cases to consider. If the format parameter is a PHP indexed array, then it denotes a sequence of values that are all parsed using the same nested format parameter. Otherwise it is a PHP associative array that denotes a dictionary sturcture. Note that the content type of the request payload may limit the range of viable representations. For example, JSON allows a top-level array representation, whereas URL-encoded data cannot denote a top-level array. When an indexed array is provided, only the first element is considered. This first element is a nested format parameter denoting how each element in the payload sequence should be verified. Example: ['s'] (accept a list of strings) Example: [['first' => 's','last' => 's']] (accept a list of dictionaries) When an associative array is provided, it is interpreted as a dictionary structure to verify. Keys may be optional if suffixed with a '?'. Example: Parse patron registration record object from request payload [ 'name' => [ 'first' => 's', 'middle?' => 's', 'last' => 's', ], 'addressLines' => ['s'], 'phone' => 's', 'dob' => 't', ] Where 't' is a custom