342 lines
13 KiB
ReStructuredText
342 lines
13 KiB
ReStructuredText
Router
|
|
======
|
|
|
|
A router organizes the components of a site by associating URI paths with handlers_. When the router receives a request, it examines the request's URI, determines which "route" matches, and dispatches the associated handler_. The handler_ is then responsible for reacting to the request and providing a response.
|
|
|
|
A typical WellRESTed Web service will have a single point of entry (usually ``/index.php``) that the Web server directs all traffic to. This script instantiates a ``Router``, populates it with routes_, and dispatches the request. Here's an example:
|
|
|
|
.. code-block:: php
|
|
|
|
<?php
|
|
|
|
use pjdietz\WellRESTed\Response;
|
|
use pjdietz\WellRESTed\Router;
|
|
|
|
require_once "vendor/autoload.php";
|
|
|
|
// Create a new router.
|
|
$router = new Router();
|
|
|
|
// Populate the router with routes.
|
|
$router->add(
|
|
["/", "\\MyApi\\RootHandler"],
|
|
["/cats/", "\\MyApi\\CatHandler"],
|
|
["/dogs/*", "\\MyApi\\DogHandler"],
|
|
["/guinea-pigs/{id}", "\\MyApi\\GuineaPigHandler"],
|
|
["~/hamsters/([0-9]+)~", "\\MyApi\\HamsterHandler"]
|
|
);
|
|
|
|
// Output a response based on the request sent to the server.
|
|
$router->respond();
|
|
|
|
Adding Routes
|
|
^^^^^^^^^^^^^
|
|
|
|
Use the ``Router::add`` method to associate a URI path with a handler_.
|
|
|
|
Here we are specifying that requests for the root path ``/`` should be handled by the class with the name ``MyApi\RootHandler``.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/", "\\MyApi\\RootHandler");
|
|
|
|
You can add routes individually, or you can add multiple routes at once. When adding multiple routes, pass a series of arrays to ``Router::add`` where each array's first item is the path and the second is the handler_.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add(
|
|
["/", "\\MyApi\\RootHandler"],
|
|
["/cats/", "\\MyApi\\CatHandler"],
|
|
["/dogs/*", "\\MyApi\\DogHandler"],
|
|
["/guinea-pigs/{id}", "\\MyApi\\GuineaPigHandler"],
|
|
["~/hamsters/([0-9]+)~", "\\MyApi\\HamsterHandler"]
|
|
);
|
|
|
|
.. note::
|
|
|
|
WellRESTed provides several types of routes including routes that match paths by regular expressions and routes that match by URI templates. See Routes_ to learn more about the different types of routes available.
|
|
|
|
Specifying Handlers
|
|
^^^^^^^^^^^^^^^^^^^
|
|
|
|
When the router finds a route that matches the request, it dispatches the associated handler_ (typically a class that implements HandlerInterface_). When adding routes (or `error handlers`_), you can specify the handler_ to dispatch in a number of ways.
|
|
|
|
.. _error handlers: `Error Handling`_
|
|
|
|
Fully Qualified Class Name (FQCN)
|
|
---------------------------------
|
|
|
|
Specify a class by FQCN by passing a string as the second parameter.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/cats/{id}", "\\MyApi\\CatHandler");
|
|
|
|
Handlers_ specified by FQCN are not instantiated (or even autoloaded) immediately. The router waits until it identifies that a request should be dispatched to a handler specified by FQCN. Then, it creates an instance of the specified class. Finally, the router calls the handler instance's ``getResponse`` method (declared in HandlerInterface_) and outputs the returned response.
|
|
|
|
Because the instantiation and autoloading are delayed, a router with 100 routes_ will still only autoload and instantiate one handler_ class throughout any individual request-response cycle.
|
|
|
|
|
|
Callable
|
|
--------
|
|
|
|
You can also use a callable to instantiate and return the handler_.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/cats/{id}", function () {
|
|
return new \MyApi\CatItemHandler();
|
|
});
|
|
|
|
This still delays instantiation, but gives you some added flexibility. For example, you could define a handler_ class that receives some configuration upon construction.
|
|
|
|
.. code-block:: php
|
|
|
|
$container = new MySuperCoolDependencyContainer();
|
|
|
|
$router->add("/cats/{id}", function () use ($container) {
|
|
return new \MyApi\CatItemHandler($container);
|
|
});
|
|
|
|
This is one approach to `dependency injection`_.
|
|
|
|
You can also return a response directly from a callable. The callables actually serve as informal handlers_ and receive the same arguments as ``HandlerInterface::getResponse``.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/hello/{name}", function ($rqst, $args) {
|
|
$name = $args["name"];
|
|
$response = new \pjdietz\WellRESTed\Response();
|
|
$response->setStatusCode(200);
|
|
$response->setBody("Hello, $name!");
|
|
return $response;
|
|
});
|
|
|
|
Instance
|
|
--------
|
|
|
|
The simplest way to use a handler_ is to instantiate it yourself and pass the instance.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/cats/{id}", new \MyApi\CatItemHandler());
|
|
|
|
This is easy, but has a significant disadvantage over the other options because each handler_ used this way will be autoloaded and instantiated, even though only one handler_ will actually be used for a given request-response cycle. You may find this approach useful for testing, but avoid if for production code.
|
|
|
|
Error Handling
|
|
^^^^^^^^^^^^^^
|
|
|
|
Use ``Router::setErrorHandler`` to provide responses for a specific status codes. The first argument is the integer status code; the second is a handler_, provided in one of the forms listed in the `Specifying Handlers`_ section.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->setErrorHandler(400, "\\MyApi\\BadRequestHandler");
|
|
$router->setErrorHandler(401, function () {
|
|
return new \MyApi\UnauthorizedHandler();
|
|
});
|
|
$router->setErrorHandler(403, function () {
|
|
$response = new \pjdietz\WellRESTed\Response(403);
|
|
$response->setBody("YOU SHALL NOT PASS!");
|
|
return $response;
|
|
});
|
|
$router->setErrorHandler(404, new \MyApi\NotFoundHandler());
|
|
|
|
You can also set multiple error handlers_ at once by passing a hash array to ``Router::setErrorHandlers``. The hash array must have integer keys representing status codes and handlers_ as values.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->setErrorHandlers([
|
|
400 => "\\MyApi\\BadRequestHandler",
|
|
401 => function () {
|
|
return new \MyApi\UnauthorizedHandler();
|
|
},
|
|
403 => function () {
|
|
$response = new \pjdietz\WellRESTed\Response(403);
|
|
$response->setBody("YOU SHALL NOT PASS!");
|
|
return $response;
|
|
},
|
|
404 => new \MyApi\NotFoundHandler()
|
|
]);
|
|
|
|
.. note::
|
|
|
|
Only one error handler_ may be registered for a given status code. A subsequent call to set the handler for a given status code will replace the previous handler with the new handler.
|
|
|
|
Registering a ``404`` handler_ will set the default behavior for when no routes in the router match. A request for ``/birds/`` using the following router will provide a response with a ``404 Not Found`` status and a message body of "I can't find anything at /birds/".
|
|
|
|
.. code-block:: php
|
|
|
|
$router = new \pjdietz\WellRESTed\Router();
|
|
$router->add(
|
|
["/cats/", $catHandler],
|
|
["/dogs/", $dogHandler]
|
|
);
|
|
$router->setErrorHandler(404, function ($rqst, $args) {
|
|
$resp = new \pjdietz\WellRESTed\Response(404);
|
|
$resp->setBody("I can't find anything at " . $rqst->getPath());
|
|
return $resp;
|
|
})
|
|
$router->respond();
|
|
|
|
HTTP Exceptions
|
|
^^^^^^^^^^^^^^^
|
|
|
|
When things go wrong, you can return responses with error codes from any of you handlers_, or you can throw an ``HttpException``. The router will catch any exceptions of this type and provide a response with the corresponding status code.
|
|
|
|
.. code-block:: php
|
|
|
|
$router->add("/cats/{catId}", function ($rqst, $args) {
|
|
|
|
// Find a cat in the cat repository.
|
|
$catProvider = new CatProvider();
|
|
$cat = $catProvider->getCatById($args["catId");
|
|
|
|
// Throw a NotFoundException if $cat is null.
|
|
if (is_null($cat)) {
|
|
throw new \pjdietz\WellRESTed\Exceptions\HttpExceptions\NotFoundException();
|
|
}
|
|
|
|
// Do cat stuff and return a response...
|
|
// ...
|
|
|
|
});
|
|
|
|
The HttpExceptions are all in the ``\pjdietz\WellRESTed\Exceptions\HttpExceptions`` namespace and all inherit from ``HttpException``. Here's the list of exceptions and their status codes.
|
|
|
|
=========== =========
|
|
Status Code Exception
|
|
=========== =========
|
|
400 BadRequestException
|
|
401 UnauthorizedException
|
|
403 ForbiddenException
|
|
404 NotFoundException
|
|
405 MethodNotAllowed
|
|
409 ConflictException
|
|
410 GoneException
|
|
500 HttpException
|
|
=========== =========
|
|
|
|
If you need to trigger an error other than these, throw ``HttpException`` and set the code, and optionally, the message.
|
|
|
|
.. code-block:: php
|
|
|
|
throw new \pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException("Request Timeout", 408);
|
|
|
|
Nested Routers
|
|
^^^^^^^^^^^^^^
|
|
|
|
For large sites, you may want to break your router into multiple subrouters. Since ``Router`` implements HandlerInterface_, you can use ``Router`` instances as handlers_. Here are a couple patterns for using subrouters.
|
|
|
|
Using Router Subclasses
|
|
-----------------------
|
|
|
|
One way to build subrouters is by subclassing ``Router`` for each subsection of your API. By subclassing, you can define a router that populates itself with routes on instantiation, and is able to be instantiated by a top-level router.
|
|
|
|
Here's a top-level router that directs traffic starting with ``/cats/`` to ``MyApi\CatRouter`` and traffic starting with ``/dogs/`` to ``MyApi\DogRouter``.
|
|
|
|
.. code-block:: php
|
|
|
|
$router = new \pjdietz\WellRESTed\Router();
|
|
$router->add(
|
|
["/cats/*", "\\MyApi\\CatRouter"],
|
|
["/dogs/*", "\\MyApi\\DogRouter"]
|
|
);
|
|
|
|
Here are router subclasses that contain only routes beginning with the expected prefixes.
|
|
|
|
.. code-block:: php
|
|
|
|
namesapce MyApi;
|
|
|
|
class CatRouter extends \pjdietz\WelRESTed\Router
|
|
{
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$ns = __NAMESPACE__;
|
|
$this->add([
|
|
"/cats/", "$ns\\CatRootHandler",
|
|
"/cats/{id}", "$ns\\CatItemHandler",
|
|
// ... other handles related to cats...
|
|
]);
|
|
}
|
|
}
|
|
|
|
.. code-block:: php
|
|
|
|
namesapce MyApi;
|
|
|
|
class DogRouter extends \pjdietz\WelRESTed\Router
|
|
{
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$ns = __NAMESPACE__;
|
|
$this->add([
|
|
"/dogs/", "$ns\\DogRootHandler",
|
|
"/dogs/{group}/", "$ns\\DogGroupHandler",
|
|
"/dogs/{group}/{breed}", "$ns\\DogBreedHandler",
|
|
// ... other handles related to dogs...
|
|
]);
|
|
}
|
|
}
|
|
|
|
With this setup, the top-level router will autoload and instantiate a ``CatHandler`` or ``DogHandler`` only if the request matches, then dispatch the request to the newly instantiated subrouter.
|
|
|
|
Using a Dependency Container
|
|
----------------------------
|
|
|
|
A second approach to subrouters is to use a dependency container such a Pimple_. A container like Pimple allows you to create "providers" that instantiate and return instances of your various routers and handlers_ as needed. As with the subclassing patten, this pattern delays autoloading and instantiating the classes until they are actually used.
|
|
|
|
.. code-block:: php
|
|
|
|
$c = new Pimple\Container();
|
|
|
|
// Create a provider for the top-level router.
|
|
// This will return an instance.
|
|
$c["router"] = (function ($c) {
|
|
$router = new \pjdietz\WellRESTed\Router();
|
|
$router->add(
|
|
["/cats/*", $c["catRouter"]],
|
|
["/dogs/*", $c["dogRouter"]]
|
|
);
|
|
return $router;
|
|
});
|
|
|
|
// Create "protected" providers for the subrouters.
|
|
// These will return callables that will return the routers when called.
|
|
$c["catRouter"] = $c->protect(function () use ($c) {
|
|
$router = new \pjdietz\WellRESTed\Router();
|
|
$router->add(
|
|
"/cats/", $c["catRootHandler"],
|
|
"/cats/{id}", $c["catItemHandler"],
|
|
// ... other handles related to cats...
|
|
]);
|
|
return $router;
|
|
});
|
|
|
|
$c["dogRouter"] = $c->protect(function () use ($c) {
|
|
$router = new \pjdietz\WellRESTed\Router();
|
|
$router->add(
|
|
"/dogs/", $c["dogRootHandler"],
|
|
"/dogs/{group}/", $c["dogGroupHandler"],
|
|
"/dogs/{group}/{breed}", $c["dogBreedHandler"],
|
|
// ... other handles related to dogs...
|
|
]);
|
|
return $router;
|
|
});
|
|
|
|
// ... Handlers like catRootHandler have protected providers as well.
|
|
|
|
See `Dependency Injection`_ for more information.
|
|
|
|
.. _Dependency Injection: dependency-injection.html
|
|
.. _handler: Handlers_
|
|
.. _Handlers: handlers.html
|
|
.. _HandlerInterface: handlers.html#handlerinterface
|
|
.. _Pimple: http://pimple.sensiolabs.org
|
|
.. _Requests: requests.html
|
|
.. _Responses: responses.html
|
|
.. _Routes: routes.html
|
|
.. _Specifying Handlers: #specifying-handlers
|