From e3e98377c33b8a7eb99ef1003b56c02f7fa409ea Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Fri, 16 Aug 2013 18:59:05 -0400 Subject: [PATCH] Track route depth on the Request, not on the Router. This allows a Handler to use the top-level router and have the depth count match the depth for the current request, not the total. --- README.md | 33 +++- src/pjdietz/WellRESTed/Handler.php | 6 +- .../Interfaces/RoutableInterface.php | 18 +++ .../Interfaces/RouteTargetInterface.php | 10 +- src/pjdietz/WellRESTed/Request.php | 150 ++++++++++-------- src/pjdietz/WellRESTed/RouteTarget.php | 10 +- src/pjdietz/WellRESTed/Router.php | 52 +++--- 7 files changed, 167 insertions(+), 112 deletions(-) create mode 100644 src/pjdietz/WellRESTed/Interfaces/RoutableInterface.php diff --git a/README.md b/README.md index ea3a5c7..675f5ab 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ $ curl -s https://getcomposer.org/installer | php $ php composer.phar install ``` -You can now use WellRESTed by including the **autoload.php** file generated by Composer. (vendor/autoload.php) +You can now use WellRESTed by including the **autoload.php** file generated by Composer. `vendor/autoload.php` Examples @@ -55,11 +55,11 @@ $response = $myRouter->getResponse(); $response->respond(); ``` -When you create your Handler subclass, you will provide a method for each HTTP verb you would like the endpoint to support. For example, if **/things/** should support GET, you would override the get() method. For POST, post(), etc. +When you create your Handler subclass, you will provide a method for each HTTP verb you would like the endpoint to support. For example, if `/things/` should support `GET`, you would override the `get()` method. For `POST`, `post()`, etc. -If your endpoint should reject particular verbs, no worries. The Handler base class defines the default verb-handling methods to respond with a 405 Method Not Allowed status. +If your endpoint should reject particular verbs, no worries. The Handler base class defines the default verb-handling methods to respond with a **405 Method Not Allowed** status. -Here's a simple Handler that matches the first endpoint, **/things/**. +Here's a simple Handler that matches the first endpoint, `/things/`. ```php class ThingCollectionHandler extends \pjdietz\WellRESTed\Handler @@ -88,7 +88,7 @@ class ThingCollectionHandler extends \pjdietz\WellRESTed\Handler } ``` -This Handler works with the second endpoint, **/things/{id}**. The pattern for this endpoint has the variable **{id}** in it. The Handler can access path variables through its **args** member, which is an associative array of variables from the URI. +This Handler works with the second endpoint, `/things/{id}`. The pattern for this endpoint has the variable `{id}` in it. The Handler can access path variables through its `args` member, which is an associative array of variables from the URI. ```php class ThingItemHandler extends \pjdietz\WellRESTed\Handler @@ -113,6 +113,23 @@ class ThingItemHandler extends \pjdietz\WellRESTed\Handler } ``` +As of version 1.3.0, you can have a Router dispatch other Routers. This is useful for breaking your endpoints into smaller route tables. Here's an example: + +```php +/** + * Top-level router that dispatches all traffic to /cat/... to CatRouter and all + * traffic to /dog/... to DogRouter. + */ +class TopRouter extends \pjdietz\WellRESTed\Router +{ + public function __construct() + { + $this->addRoute(new Route('/^\/cat\//', 'CatRouter')); + $this->addRoute(new Route('/^\/dog\//', 'DogRouter')); + } +} +``` + ### Requests and Responses @@ -130,6 +147,8 @@ exit; The Request class goes hand-in-hand with the Response class. Again, this is used in the Handler class to read the information from the request being handled. From outside the context of a Handler, you can also use the Request class to read info for the request sent to the server. ```php +// Call the static method getRequest() to get a reference to the Request +// singleton that represents the request made to the server. $rqst = \pjdietz\WellRESTed\Request::getRequest(); if ($rqst->getMethod() === 'PUT') { @@ -161,7 +180,7 @@ if ($resp->getStatusCode() === 201) { ### Routing from a Handler -One more fun thing you can do with WellRESTed is use your API from inside your API. WellRESTed makes this easy because each dispatched Handler keeps a reference to the Router that dispatched it. +One more fun thing you can do with WellRESTed is use your API from inside your API. WellRESTed makes this easy because each dispatched Handler or Router keeps a reference to the top-level Router. Suppose you have an endpoint that needs to look up information using another endpoint. Now that you've seen that you can create and work with your own requests and responses, we'll look at how to use them in the context of a handler. @@ -197,4 +216,4 @@ For more examples, see the project [pjdietz/wellrested-samples](https://github.c Copyright and License --------------------- Copyright © 2013 by PJ Dietz -Licensed under the [MIT license](http://opensource.org/licenses/MIT) \ No newline at end of file +Licensed under the [MIT license](http://opensource.org/licenses/MIT) diff --git a/src/pjdietz/WellRESTed/Handler.php b/src/pjdietz/WellRESTed/Handler.php index c9f7f99..317c838 100644 --- a/src/pjdietz/WellRESTed/Handler.php +++ b/src/pjdietz/WellRESTed/Handler.php @@ -11,8 +11,8 @@ namespace pjdietz\WellRESTed; use pjdietz\WellRESTed\Interfaces\HandlerInterface; -use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface; +use pjdietz\WellRESTed\Interfaces\RoutableInterface; /** * A Handler issues a response for a given resource. @@ -22,10 +22,10 @@ use pjdietz\WellRESTed\Interfaces\ResponseInterface; abstract class Handler extends RouteTarget implements HandlerInterface { /** - * @param RequestInterface $request + * @param RoutableInterface $request * @return ResponseInterface */ - public function getResponse(RequestInterface $request = null) + public function getResponse(RoutableInterface $request = null) { if (!is_null($request)) { $this->request = $request; diff --git a/src/pjdietz/WellRESTed/Interfaces/RoutableInterface.php b/src/pjdietz/WellRESTed/Interfaces/RoutableInterface.php new file mode 100644 index 0000000..932062b --- /dev/null +++ b/src/pjdietz/WellRESTed/Interfaces/RoutableInterface.php @@ -0,0 +1,18 @@ +setHostname($host); - - $path = isset($parsed['path']) ? $parsed['path'] : ''; - $this->setPath($path); - - $port = isset($parsed['port']) ? (int) $parsed['port'] : 80; - $this->setPort($port); - - $query = isset($parsed['query']) ? $parsed['query'] : ''; - $this->setQuery($query); - } /** * Return a reference to the singleton instance of the Request derived @@ -115,24 +94,6 @@ class Request extends Message implements RequestInterface return self::$theRequest; } - /** - * Set instance members based on the HTTP request sent to the server. - */ - public function readHttpRequest() - { - $this->setBody(file_get_contents("php://input"), false); - $this->headers = self::getRequestHeaders(); - - // Add case insensitive headers to the lookup table. - foreach ($this->headers as $key => $value) { - $this->headerLookup[strtolower($key)] = $key; - } - - $this->method = $_SERVER['REQUEST_METHOD']; - $this->uri = $_SERVER['REQUEST_URI']; - $this->hostname = $_SERVER['HTTP_HOST']; - } - /** @return array all request headers from the current request. */ public static function getRequestHeaders() { @@ -165,6 +126,24 @@ class Request extends Message implements RequestInterface return $arh; } + /** + * Set instance members based on the HTTP request sent to the server. + */ + public function readHttpRequest() + { + $this->setBody(file_get_contents("php://input"), false); + $this->headers = self::getRequestHeaders(); + + // Add case insensitive headers to the lookup table. + foreach ($this->headers as $key => $value) { + $this->headerLookup[strtolower($key)] = $key; + } + + $this->method = $_SERVER['REQUEST_METHOD']; + $this->uri = $_SERVER['REQUEST_URI']; + $this->hostname = $_SERVER['HTTP_HOST']; + } + /** * Return the hostname portion of the URI * @@ -297,6 +276,67 @@ class Request extends Message implements RequestInterface } } + /** + * Return the full URI includeing protocol, hostname, path, and query. + * + * @return array + */ + public function getUri() + { + $uri = strtolower($this->protocol) . '://' . $this->hostname; + + if ($this->port !== 80) { + $uri .= ':' . $this->port; + } + + $uri .= $this->path; + + if ($this->query) { + $uri .= '?' . http_build_query($this->query); + } + + return $uri; + } + + /** + * Set the URI for the Request. This sets the other members: hostname, + * path, port, and query. + * + * @param string $uri + */ + public function setUri($uri) + { + $parsed = parse_url($uri); + + $host = isset($parsed['host']) ? $parsed['host'] : ''; + $this->setHostname($host); + + $path = isset($parsed['path']) ? $parsed['path'] : ''; + $this->setPath($path); + + $port = isset($parsed['port']) ? (int) $parsed['port'] : 80; + $this->setPort($port); + + $query = isset($parsed['query']) ? $parsed['query'] : ''; + $this->setQuery($query); + } + + // ------------------------------------------------------------------------- + + /** @return int The number of times a router has dispatched this Routable */ + public function getRouteDepth() + { + return $this->routeDepth; + } + + /** Increase the instance's internal count of its depth in nested route tables */ + public function incrementRouteDepth() + { + $this->routeDepth++; + } + + // ------------------------------------------------------------------------- + /** * Make a cURL request out of the instance and return a Response. * @@ -382,28 +422,4 @@ class Request extends Message implements RequestInterface return $resp; } - // ------------------------------------------------------------------------- - - /** - * Return the full URI includeing protocol, hostname, path, and query. - * - * @return array - */ - public function getUri() - { - $uri = strtolower($this->protocol) . '://' . $this->hostname; - - if ($this->port !== 80) { - $uri .= ':' . $this->port; - } - - $uri .= $this->path; - - if ($this->query) { - $uri .= '?' . http_build_query($this->query); - } - - return $uri; - } - } diff --git a/src/pjdietz/WellRESTed/RouteTarget.php b/src/pjdietz/WellRESTed/RouteTarget.php index 74d21dd..131b2a3 100644 --- a/src/pjdietz/WellRESTed/RouteTarget.php +++ b/src/pjdietz/WellRESTed/RouteTarget.php @@ -10,8 +10,8 @@ namespace pjdietz\WellRESTed; -use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface; +use pjdietz\WellRESTed\Interfaces\RoutableInterface; use pjdietz\WellRESTed\Interfaces\RouterInterface; use pjdietz\WellRESTed\Interfaces\RouteTargetInterface; @@ -25,7 +25,7 @@ abstract class RouteTarget implements RouteTargetInterface { /** @var array Matches array from the preg_match() call used to find this Handler */ protected $args; - /** @var RequestInterface The HTTP request to respond to. */ + /** @var RoutableInterface The HTTP request to respond to. */ protected $request; /** @var ResponseInterface The HTTP response to send based on the request. */ protected $response; @@ -44,14 +44,14 @@ abstract class RouteTarget implements RouteTargetInterface return $this->args; } - /** @return RequestInterface */ + /** @return RoutableInterface */ public function getRequest() { return $this->request; } - /** @param RequestInterface $request */ - public function setRequest(RequestInterface $request) + /** @param RoutableInterface $request */ + public function setRequest(RoutableInterface $request) { $this->request = $request; } diff --git a/src/pjdietz/WellRESTed/Router.php b/src/pjdietz/WellRESTed/Router.php index eb7cdcf..544824e 100644 --- a/src/pjdietz/WellRESTed/Router.php +++ b/src/pjdietz/WellRESTed/Router.php @@ -10,8 +10,8 @@ namespace pjdietz\WellRESTed; -use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface; +use pjdietz\WellRESTed\Interfaces\RoutableInterface; use pjdietz\WellRESTed\Interfaces\RouteInterface; use pjdietz\WellRESTed\Interfaces\RouterInterface; use pjdietz\WellRESTed\Interfaces\RouteTargetInterface; @@ -31,8 +31,6 @@ class Router extends RouteTarget implements RouterInterface protected $maxDepth = self::MAX_DEPTH; /** @var array Array of Route objects */ private $routes; - /** @var int counter incrememted each time a router dispatches a route target. */ - private $depth = 0; /** Create a new Router. */ public function __construct() @@ -50,13 +48,21 @@ class Router extends RouteTarget implements RouterInterface $this->routes[] = $route; } + /** + * @return int maximum levels of routing before the router raises an error. + */ + public function getMaxDepth() + { + return $this->maxDepth; + } + /** * Return the response built by the handler based on the request * - * @param RequestInterface $request + * @param RoutableInterface $request * @return ResponseInterface */ - public function getResponse(RequestInterface $request = null) + public function getResponse(RoutableInterface $request = null) { // Set the instance's request, if the called passed one. if (!is_null($request)) { @@ -68,8 +74,13 @@ class Router extends RouteTarget implements RouterInterface } // Reference the request and path. $request = $this->request; + $request->incrementRouteDepth(); $path = $request->getPath(); + if ($request->getRouteDepth() >= $this->getMaxDepth()) { + return $this->getInternalServerErrorResponse($request, 'Maximum route recursion reached.'); + } + foreach ($this->routes as $route) { /** @var RouteInterface $route */ if (preg_match($route->getPattern(), $path, $matches)) { @@ -80,7 +91,7 @@ class Router extends RouteTarget implements RouterInterface $target = new $targetClassName(); $target->setRequest($request); - // If this instance already had argument, merge the matches with them. + // If this instance already had arguments, merge the matches with them. $myArguments = $this->getArguments(); if (!is_null($myArguments)) { $matches = array_merge($myArguments, $matches); @@ -90,10 +101,8 @@ class Router extends RouteTarget implements RouterInterface // If this instance already had a top-level router, pass it along. // Otherwise, pass itself as the top-level router. if (isset($this->router)) { - $this->router->incrementDepth(); $target->setRouter($this->router); } else { - $this->incrementDepth(); $target->setRouter($this); } @@ -108,34 +117,27 @@ class Router extends RouteTarget implements RouterInterface return $this->getNoRouteResponse($request); } - public function incrementDepth() - { - $this->depth++; - if ($this->depth >= $this->maxDepth) { - $response = $this->getInternalServerErrorResponse( - $this->getRequest(), - 'Maximum recursion level reached.' - ); - $response->respond(); - exit; - } - } - /** * Prepare a resonse indicating a 404 Not Found error * - * @param RequestInterface $request + * @param RoutableInterface $request * @return ResponseInterface */ - protected function getNoRouteResponse(RequestInterface $request) + protected function getNoRouteResponse(RoutableInterface $request) { $response = new Response(404); $response->body = 'No resource at ' . $request->getPath(); return $response; } - /** Prepare a response indicating a 500 Internal Server Error */ - protected function getInternalServerErrorResponse(RequestInterface $request, $message = '') + /** + * Prepare a response indicating a 500 Internal Server Error + * + * @param RoutableInterface $request + * @param string $message Optional additional message. + * @return ResponseInterface + */ + protected function getInternalServerErrorResponse(RoutableInterface $request, $message = '') { $response = new Response(500); $response->setBody('Server error at ' . $request->getPath() . "\n" . $message);