A simple routing library


License
MIT

Documentation

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