From 6acd7c44a160fc4184f2ebc003ba2fabcabbba7b Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Sat, 16 May 2015 11:19:48 -0400 Subject: [PATCH] Rewrite documentation for version 3.0 --- README.md | 109 +++-- docs/source/_static/css/style.css | 10 + docs/source/conf.py | 6 +- docs/source/dependency-injection.rst | 100 ++-- docs/source/getting-started.rst | 353 +++++-------- docs/source/handlers.rst | 234 --------- docs/source/index.rst | 106 +++- docs/source/middleware.rst | 295 +++++++++++ docs/source/overview.rst | 10 +- docs/source/router.rst | 599 ++++++++++++----------- docs/source/routes.rst | 189 ------- docs/source/uri-templates.rst | 181 +++++++ docs/source/web-server-configuration.rst | 3 +- 13 files changed, 1149 insertions(+), 1046 deletions(-) create mode 100644 docs/source/_static/css/style.css delete mode 100644 docs/source/handlers.rst create mode 100644 docs/source/middleware.rst delete mode 100644 docs/source/routes.rst create mode 100644 docs/source/uri-templates.rst diff --git a/README.md b/README.md index d7a9a84..0f22cb7 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,93 @@ WellRESTed ========== -[![Build Status](https://travis-ci.org/pjdietz/wellrested.svg?branch=master)](https://travis-ci.org/pjdietz/wellrested) +[![Build Status](https://travis-ci.org/wellrestedphp/wellrested.svg?branch=psr7)](https://travis-ci.org/wellrestedphp/wellrested) +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/b0a2efcb-49f8-4a90-a5bd-0c14e409f59e/mini.png)](https://insight.sensiolabs.com/projects/b0a2efcb-49f8-4a90-a5bd-0c14e409f59e) -WellRESTed is a micro-framework for creating RESTful APIs in PHP. It provides a lightweight yet powerful routing system and classes to make working with HTTP requests and responses clean and easy. +WellRESTed is a library for creating RESTful Web services in PHP. Requirements ------------ -- PHP 5.3 -- [PHP cURL](http://php.net/manual/en/book.curl.php) for making requests with the [`Client`](src/pjdietz/WellRESTed/Client.php) class (Optional) - +- PHP 5.4 Install ------- -Add an entry for "pjdietz/wellrested" to your composer.json file's `require` property. If you are not already using Composer, create a file in your project called "composer.json" with the following content: +Add an entry for "wellrested/wellrested" to your composer.json file's `require` property. ```json { "require": { - "pjdietz/wellrested": "~2.3" + "pjdietz/wellrested": "~3.0" } } ``` -Use Composer to download and install WellRESTed. Run these commands from the directory containing the **composer.json** file. - -```bash -$ curl -s https://getcomposer.org/installer | php -$ php composer.phar install -``` - -You can now use WellRESTed by including the `vendor/autoload.php` file generated by Composer. - - -Overview --------- - -WellRESTed's primary goal is to facilitate mapping of URIs to classes that will provide or accept representations. To do this, create a [`Router`](src/pjdietz/WellRESTed/Router.php) instance and load it up with some routes. - -```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(); -``` - Documentation ------------- See [the documentation](http://wellrested.readthedocs.org/en/stable/) to get started. +Example +------- + +```php +getAttribute("name", "world"); + + // Set the response body to the greeting and the status code to 200 OK. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, $name!")); + + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); + +}; + +// Add a header to the response. +$headerAdder = function ($request, $response, $next) { + // Add the header. + $response = $response->withHeader("X-example", "hello world"); + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); +}; + +// Create a server +$server = new Server(); + +// Start each request-response cycle by dispatching the header adder. +$server->add($headerAdder); + +// The header adder will propagate to this router, which will dispatch the +// $hello middleware, possibly with a {name} variable. +$server->add($server->createRouter() + ->register("GET", "/hello", $hello) + ->register("GET", "/hello/{name}", $hello) +); + +// Read the request from the client, dispatch middleware, and output. +$server->respond(); + +``` + + Copyright and License --------------------- Copyright © 2015 by PJ Dietz diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/style.css new file mode 100644 index 0000000..04461ca --- /dev/null +++ b/docs/source/_static/css/style.css @@ -0,0 +1,10 @@ +@import url("theme.css"); + +.code-table td { + vertical-align: top; + font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; +} + +.code { + font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 6133cd1..8684b43 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,8 +27,8 @@ master_doc = 'index' # General information about the project. project = u'WellRESTed' copyright = u'2015, PJ Dietz' -version = '2.3.0' -release = '2.3.0' +version = '3.0.0' +release = '3.0.0' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -194,3 +194,5 @@ if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +html_style = 'css/style.css' diff --git a/docs/source/dependency-injection.rst b/docs/source/dependency-injection.rst index 8adf166..e06417a 100644 --- a/docs/source/dependency-injection.rst +++ b/docs/source/dependency-injection.rst @@ -1,49 +1,40 @@ Dependency Injection ==================== -Here are a few strategies for how to do dependency injection with WellRESTed. +Here are a few strategies for how to make a dependency injection container availble to middleware with WellRESTed. -HandlerInterface::getResponse -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Request Attribute +^^^^^^^^^^^^^^^^^ -You can inject dependencies into your handlers_ by passing them into ``Router::respond`` (or ``Router::getResponse``). This array will propagate through the routes_ to your handler_, possibly gaining additional array members (like variables from a TemplateRoute_) along the way. +``Psr\Http\Message\ServerRequestInterface`` provides "attributes" that allow you attach arbitrary data to a request. You can use this to make your dependcy container available to any dispatched middleware. -Define a handler_ that expects to receive the dependency container as the "container" element of the array passed to ``getResponse``. - -.. code-block:: php - - Class CatHandler implements \pjdietz\WellRESTed\Interfaces\HandlerInterface - { - public function getResponse(RequestInterface $request, array $args = null) - { - // Extract the container from the second parameter. - $container = $args["container"]; - // Do something with the container, and make a response. - // ... - return $response; - } - } - -Create the router. Pass the the container to ``Router::respond`` as the "container" array element. +When you instatiate a ``WellRESTed\Server``, you can provide an array of attributes that the server will add to the request. .. code-block:: php $container = new MySuperCoolDependencyContainer(); - $router = new \pjdietz\WellRESTed\Router(); - $router->add("/cats", "CatHandler"); - // Pass an array containing the dependencies to Router::respond(). - $router->respond(["container" => container]); + $server = new WellRESTed\Server(["container" => $container]); + // ... Add middleware, routes, etc. ... +When the server dispatches middleware, the middleware will be able to read the contain as the "container" attribute. + +.. code-block:: php + + function ($request, $response, $next) { + $container = $request->getAttribute("container"); + // It's a super cool dependency container! + } Callables ^^^^^^^^^ -When using callables to provide handlers_, you have the opportunity to inject dependencies into the handler's constructor. +Another approach is to use callables that return ``MiddlewareInterface`` instances when you assign middleware. This approach provides an oppurtunity to pass the container into the middleware's constructor. + .. code-block:: php - Class CatHandler implements \pjdietz\WellRESTed\Interfaces\HandlerInterface + Class CatHandler implements WellRESTed\MiddlewareInterface { private $container; @@ -52,7 +43,7 @@ When using callables to provide handlers_, you have the opportunity to inject de $this->container = $container; } - public function getResponse(RequestInterface $request, array $args = null) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next); { // Do something with the $this->container, and make a response. // ... @@ -60,20 +51,24 @@ When using callables to provide handlers_, you have the opportunity to inject de } } - -Create the router. Pass the the container to the handler upon instantiation. +When you add the middleware to the server or register it with a router, use a callable that passes container into the contructor. .. code-block:: php $container = new MySuperCoolDependencyContainer(); - $router = new Router(); - $router->add("/cats/", function () use ($container) { + $catHandler = function () use ($container) { return new CatHandler($container); - }); - $router->respond(); + } -For extra fun (and more readable code), you could store the callable that provides the handler in the container. Here's an example using Pimple_). + $server = new Server(); + $server->add( + $server->createRoute() + ->register("GET", "/cats/{cat}", $catHandler) + ); + $server->respond(); + +For extra fun, store the callable that provides the handler in the container. Here's an example using Pimple_). .. code-block:: php @@ -82,12 +77,33 @@ For extra fun (and more readable code), you could store the callable that provid return new CatHandler($c); }); - $router = new Router(); - $router->add("/cats/", $c["catHandler"]); - $router->respond(); + $server = new Server(); + $server->add( + $server->createRoute() + ->register("GET", "/cats/{cat}", $c["catHandler"]) + ); + $server->respond(); + +Combined +^^^^^^^^ + +Of course these two approaches are not mutually exclusive. You can even obtain your server from the container as well, for good measure. + +.. code-block:: php + + $c = new Pimple\Container(); + $c["server"] = function ($c) { + return new Server(["container" => $c); + }; + $c["catHandler"] = $c->protect(function () use ($c) { + return new CatHandler($c); + }); + + $server = $c["server"]; + $server->add( + $server->createRoute() + ->register("GET", "/cats/{cat}", $c["catHandler"]) + ); + $server->respond(); -.. _Handler: Handlers_ -.. _Handlers: handlers.html .. _Pimple: http://pimple.sensiolabs.org -.. _Routes: routes.html -.. _TemplateRoute: routes.html#template-routes diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index 5f0980f..9266ae9 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -1,302 +1,193 @@ Getting Started =============== -This page provides a brief introduction to WellRESTed. We'll start with a `Hello, world!`_, take a quick peek at `using handlers`_, and finally explore reacting to `HTTP methods`_. +This page provides a brief introduction to WellRESTed. We'll take a tour of some of the features of WellRESTed without getting into too much depth. + +To start, we'll make a "`Hello, world!`_" to demonstrate the concepts of middleware and routing and show how to read variables from the request path. Hello, World! ^^^^^^^^^^^^^ -Let's start with a very basic "Hello, world!". Here, we will create a router_ that will look for requests to ``/hello`` and respond with "Hello, world!" +Let's start with a very basic "Hello, world!". Here, we will create a server. A ``WellRESTed\Server`` reads the +incoming request from the client, dispatches some middleware_, and transmits a response back to the client. + +Our middelware is a function that returns a response with the status code set to ``200`` and the body set to "Hello, world!". + +.. _`Example 1`: +.. rubric:: Example 1: Simple "Hello, world!" .. code-block:: php add("/hello", function () { - $response = new Response(); - $response->setStatusCode(200); - $response->setHeader("Content-type", "text/plain"); - $response->setBody("Hello, world!"); - return $response; + // Add middleware to dispatch that will return a response. + // In this case, we'll use an anonymous function. + $server->add(function ($request, $response, $next) { + // Update the response with the greeting, status, and content-type. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, world!")); + // Use $next to forward the request on to the next middleware, if any. + return $next($request, $response); }); // Read the request sent to the server and use it to output a response. - $router->respond(); + $server->respond(); -In this example, we created a router, then added one "route" to it. This route matches requests with the path ``/hello`` and dispatches them to a callable. The callable builds and returns a response. +.. note:: -Reading from the Request ------------------------- + The middleware in this example provides a ``Stream`` as the body instead of a string. This is a feature or PSR-7 where HTTP message bodies are always represented by streams. This allows you to work with very large bodies without having to store the entire contents in memory. -This is a good start, but it's not very dynamic. Rather than always responding with the same message, let's respond with a greeting using some information from the request's query. + WellRESTed provides ``Stream`` and ``NullStream``, but you can use any implementation of ``Psr\Http\Message\StreamInteface``. + +Routing by Path +^^^^^^^^^^^^^^^ + +This is a good start, but it provides the same response to every request. Let's provide this response only when a client sends a request to ``/hello``. + +For this, we need a router_. A router_ is a special type of middleware_ that examines the request and routes the request through to the middleware that matches. + +.. _`Example 2`: +.. rubric:: Example 2: Routed "Hello, world!" .. code-block:: php createRouter(); - // Add a route that matches the path "/hello" and returns a response. - $router->add("/hello", function (RequestInterface $request) { - - // Provide a default. - $name = "world"; - - // Read from the query. - $query = $request->getQuery(); - if (isset($query["name"])) { - $name = $query["name"]; - } - - $response = new Response(); - $response->setStatusCode(200); - $response->setHeader("Content-type", "text/plain"); - $response->setBody("Hello, $name!"); - return $response; + // Map middleware to an endpoint and method(s). + $router->register("GET", "/hello", function ($request, $response, $next) { + // Update the response with the greeting, status, and content-type. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, world!")); + // Use $next to forward the request on to the next middleware, if any. + return $next($request, $response); }); - $router->respond(); + // Add the router to the server. + $server->add($router); -With this router, requests for ``/hello?name=Molly`` will get the response "Hello, Molly" while requests for ``/hello?name=Oscar`` will get "Hello, Oscar!" + // Read the request sent to the server and use it to output a response. + $server->respond(); -The callable we passed to ``$router->add()`` receives a variable called ``$request`` that represents the request being dispatched. The ``RequestInterface`` provides access to information such as the request method (e.g., "GET", "POST", "PUT", "DELETE"), headers, the entity body, and the query. +Reading Path Variables +^^^^^^^^^^^^^^^^^^^^^^ -Reading from the Path ---------------------- +Routes can be static (like the one above that matches only ``/hello``), or they can be dynamic. Here's an example that uses a dynamic route to read a portion from the path to use as the greeting. For example, a request to ``/hello/Molly`` will respond "Hello, Molly", while a request to ``/hello/Oscar`` will respond "Hello, Oscar!" -Instead of specifying the name for the greeting in the query, let's modify our API to look for the name in the path so that a request to ``/hello/Molly`` will provide a "Hello, Molly!" response. + + +.. _`Example 3`: +.. rubric:: Example 3: Personalized "Hello, world!" .. code-block:: php add("/hello/{name}", function (RequestInterface $request, array $args) { + // Check for a "name" attribute which may have been provided as a + // path variable. The second parameters allows us to set a default. + $name = $request->getAttribute("name", "world"); - // The part of the path where {name} appear will be extracted - // and provided to the callable as the "name" array element. - $name = $args["name"]; + // Update the response with the greeting, status, and content-type. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, $name!")); - $response = new Response(); - $response->setStatusCode(200); - $response->setHeader("Content-type", "text/plain"); - $response->setBody("Hello, $name!"); - return $response; - }); - - $router->respond(); - -Notice that the first parameter passed to ``$router->add()`` is now "/hello/{name}". The curly braces define a variable that will match text in that section of the path and provide it to the callable by adding an array element. Since "name" appears inside the braces, "name" will be the key; the matched text ("Molly" for requests to ``/hello/Molly``) will be the value. - -To learn more about extracting variables from the path, see TemplateRoutes_ and RegexRoutes_. - -Using Handlers -^^^^^^^^^^^^^^ - -So far, the examples have been limited to building the entire Web service inside a single ``index.php`` file. For an actual site, you'll want to spread your code across many files. - -Let's start by replacing the callable we used above with a handler. In WellRESTed, a "handler" is a piece of middleware that takes a request and provides a response. We used callables as informal handlers above, but the more formal approach is to use HandlerInterface_. - -.. code-block:: php - - setStatusCode(200); - $response->setHeader("Content-type", "text/plain"); - $response->setBody("Hello, $name!"); - return $response; - } + return $next($request, $response); } -When we add the route to the router, we can specify the handler by providing a string containing the handler's fully qualified class name (FQCN). When the router receives a request that matches the ``/hello/{name}`` path, it will instantiate ``MyApi\HelloHandler`` and use the instance to get a response. + // Create the server and router. + $server = new Server(); + $router = $server->createRouter(); -Here's the updated ``index.php`` + // Register the middleware for an exact match to /hello + $router->register("GET", "/hello", $hello); + // Register to match a pattern with a variable. + $router->register("GET", "/hello/{name}", $hello); + + $server->add($router); + $server->respond(); + +Multiple Middleware +^^^^^^^^^^^^^^^^^^^ + +One thing we haven't seen yet is how middleware work together. For the next example, we'll use an additional middleware that sets an ``X-example: hello world``. .. code-block:: php add("/hello/{name}", "\\MyApi\\HelloHandler"); - $router->respond(); + // Set the status code and provide the greeting as the response body. + $hello = function ($request, $response, $next) { -HTTP Methods -^^^^^^^^^^^^ + // Check for a "name" attribute which may have been provided as a + // path variable. Use "world" as a default. + $name = $request->getAttribute("name", "world"); -The examples so far have not touched on HTTP methods—the "Hello, world!" handlers give the same responses for GET, POST, PUT, etc. + // Set the response body to the greeting and the status code to 200 OK. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, $name!")); -Let's set aside "Hello, world!" for now, and imagine tiny RESTful API about cats. We'll start with a ``/cats/`` endpoint that should allow these requests: + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); -``GET /cats/`` - Output a list of cat representations in JSON. + }; -``POST /cats/`` - Accept a JSON representation of a cat and store it. + // Add a header to the response. + $headerAdder = function ($request, $response, $next) { + // Add the header. + $response = $response->withHeader("X-example", "hello world"); + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); + }; -``OPTIONS /cats/`` - List the methods the endpoint allows. + // Create a server + $server = new Server(); -(We won't actually store any cats here. The example is just to show how the HTTP parts work.) + // Add $headerAdder to the server first to make it the first to run. + $server->add($headerAdder); -HandlerInterface ----------------- + // When $headerAdder calls $next, it will dispatch the router because it is + // added to the server right after. + $server->add($server->createRouter() + ->register("GET", "/hello", $hello) + ->register("GET", "/hello/{name}", $hello) + ); -We can react to the verbs using a class implementing HandlerInterface_ like this: + // Read the request from the client, dispatch middleware, and output. + $server->respond(); -.. code-block:: php - namespace MyApi; - - use pjdietz\WellRESTed\Interfaces\HandlerInterface; - use pjdietz\WellRESTed\Interfaces\RequestInterface; - use pjdietz\WellRESTed\Interfaces\ResponseInterface; - use pjdietz\WellRESTed\Response; - - class CatHandler implements HandlerInterface - { - public function getResponse(RequestInterface $request, array $args = null) - { - $response = new Response(); - - // Determine how to respond based on the request's HTTP method. - $method = $request->getMethod(); - if ($method === "GET") { - // Read the list of cats. - $cats = $this->getCats(); - $response->setStatusCode(200); - $response->setHeader("Content-type", "application/json"); - $response->setBody(json_encode($cats)); - } elseif ($method === "POST") { - // Read the cat from the request body. - $cat = json_decode($request->getBody()); - // Store it, and read the updated representation. - $newCat = $this->storeCat($cat); - $response->setStatusCode(201); - $response->setHeader("Content-type", "application/json"); - $response->setBody(json_encode($newCat)); - } elseif ($method === "OPTIONS") { - // List the methods are allowed for this endpoint. - $response->setStatusCode(200); - $response->setHeader("Allow", "GET,HEAD,POST,OPTIONS"); - } else { - // Request did not use one of the allowed verbs. - $response->setStatusCode(405); - $response->setHeader("Allow", "GET,HEAD,POST,OPTIONS"); - } - return $response; - } - - private function getCats() - { - // ...Read cats from storage... - } - - private function storeCat($cat) - { - // ...Store cats here... - } - } - -Handler Class -------------- - -Not bad, but that's a lot of branching. We can clean this up a bit by deriving our ``CatHandler`` from `pjdietz\\WellRESTed\\Handler`__, an abstract class that implements ``HandlerInterface`` and provides protected methods for you to override that correspond with HTTP methods. Here's ``CatHandler`` refactored as a Handler_ subclass. - -__ Handler_ - -.. code-block:: php - - getCats(); - $this->response->setStatusCode(200); - $this->response->setHeader("Content-type", "application/json"); - $this->response->setBody(json_encode($cats)); - } - - protected function post() - { - // Read the cat from the request body. - $cat = json_decode($this->request->getBody()); - // Store it, and read the updated representation. - $newCat = $this->storeCat($cat); - $this->response->setStatusCode(201); - $this->response->setHeader("Content-type", "application/json"); - $this->response->setBody(json_encode($newCat)); - } - - protected function getAllowedMethods() - { - return ["GET","HEAD","POST","OPTIONS"]; - } - - private function getCats() - { - // ...Read cats from storage... - } - - private function storeCat($cat) - { - // ...Store cats here... - } - } - -Using Handler_, we override the methods for verbs we want to support; all other will automatically respond ``405 Method Not Allowed``. In addition, returning an array of verbs from ``getAllowedMethods`` adds an ``Allow`` header for ``405 Method Not Allowed`` responses, and automatically provides for ``OPTIONS`` support. - -Note that when using Handler_, you interact with the request and response through the protected instance members ``$this->request`` and ``$this->response``. The array provided to ``HandlerInterface:getResponse`` as the second argument is available as ``$this->args``. - -.. _TemplateRoutes: routes.html#template-routes -.. _RegexRoutes: routes.html#regex-routes +.. _middleware: middleware.html .. _router: router.html -.. _HandlerInterface: handlers.html#handlerinterface -.. _Handler: handlers.html#handler-class diff --git a/docs/source/handlers.rst b/docs/source/handlers.rst deleted file mode 100644 index 59febcc..0000000 --- a/docs/source/handlers.rst +++ /dev/null @@ -1,234 +0,0 @@ -Handlers -======== - -Handlers serve as bridges between requests and responses. A handler is where you will do things such as retrieve a representation to send as the response or read a representation submitted with a request. - -HandlerInterface -^^^^^^^^^^^^^^^^ - -The HandlerInterface_ is used throughout WellRESTed. It's a very simple and has just one method: - -.. code-block:: php - - /** - * Return the handled response. - * - * @param \pjdietz\WellRESTed\RequestInterface $request The request to respond to. - * @param array|null $args Optional additional arguments. - * @return \pjdietz\WellRESTed\ResponseInterface The handled response. - */ - public function getResponse(RequestInterface $request, array $args = null); - -To create a handler, define a class that implements this interface. Use the ``getResponse`` method to inspect the request and return an appropriate response. - -.. code-block:: php - - use pjdietz\WellRESTed\Interfaces\HandlerInterface; - use pjdietz\WellRESTed\Interfaces\RequestInterface; - use pjdietz\WellRESTed\Response; - - class HelloHandler implements HandlerInterface - { - public function getResponse(RequestInterface $request, array $args = null) - { - $response = new Response(); - $response->setStatusCode(200); - $response->setHeader("Content-type", "text/plain"); - $response->setBody("Hello, world!"); - return $response; - } - } - -Request -------- - -``getResponse()`` receives two arguments when it is called. The first argument represents the request. The handler can use this to read information such as the HTTP method, headers, query parameters, and entity body. - -Here's a handler that reads a representation sent with a ``POST`` request. - -.. code-block:: php - - use pjdietz\WellRESTed\Interfaces\HandlerInterface; - use pjdietz\WellRESTed\Interfaces\RequestInterface; - use pjdietz\WellRESTed\Response; - - class DogHandler implements HandlerInterface - { - public function getResponse(RequestInterface $request, array $args = null) - { - $response = new Response(); - - // Read the request method. - $method = $request->getMethod(); - if ($method === "POST") { - - // Read the request body. - $dog = json_decode($request->getBody()); - - // ...Store the representation... - - // Provide the response. - $response = new Response(); - $response->setStatusCode(201); - $response->setHeader("Content-type", "application/json"); - $response->setBody(json_encode($newDog); - - } elseif ($method === "OPTIONS") { - - // List the methods are allowed for this endpoint. - $response->setStatusCode(200); - $response->setHeader("Allow", "POST,OPTIONS"); - - } else { - - // Request did not use one of the allowed verbs. - $response->setStatusCode(405); - $response->setHeader("Allow", "POST,OPTIONS"); - - } - return $response; - } - } - -Array ------ - -The second argument is an array of extra data such as variables from a TemplateRoute_ or captures from a RegexRoute_. - -Suppose you want an endpoint that will represent one specific cat by ID which the user can read using a ``GET`` request. The endpoint should match requests for paths such as ``/cats/123`` or ``/cats/2``. - -Use a TemplateRoute_ to extract the ID from the path. - -.. code-block:: php - - // Use a TemplateRoute to extract the ID from the path. - $router->add("/cats/{id}", "\\MyApi\\CatItemHandler"); - -The extracted ID will be made available to the handler as an array element in the second argument sent to ``getResponse``. - -.. code-block:: php - - namesapce MyApi; - - Class CatItemHandler implements HandlerInterface - { - public function getResponse(RequestInterface $request, array $args = null) - { - $response = new Response(); - - // Determine how to respond based on the request's HTTP method. - $method = $rqst->getMethod(); - if ($method === "GET") { - // Lookup the cat using the ID from the path. - $cat = $this->getCatById($args["id"])); - if ($cat) { - // Respond with a representation. - $response->setStatusCode(200); - $response->setHeader("Content-type", "application/json"); - $response->setBody(json_encode($cat)); - } else { - // Or, a Not Found error if there's no cat with that ID. - $response->setStatusCode(404); - } - } elseif ($method === "OPTIONS") { - // User wants to know what verbs are allowed for this endpoint. - $response->setStatusCode(200); - $response->setHeader("Allow", "GET,HEAD,OPTIONS"); - } else { - // User did not use one of the allowed verbs. - $response->setStatusCode(405); - $response->setHeader("Allow", "GET,HEAD,OPTIONS"); - } - return $response; - } - - private getCatById($id) - { - // ... Lookup the cat from storage and return it. - } - } - -Handler Class -^^^^^^^^^^^^^ - -When you write your handlers, you can either write them from scratch and implement ``HandlerInterface`` as we did above, or you can extend the abstract ``Handler`` class which provides some convenient features such as instance members for the request and response and methods for the most common HTTP verbs. - -Instance Members ----------------- - -============ ==================== =================================================================== -Member Type Description -============ ==================== =================================================================== -``args`` ``array`` Hash to supplement the request; usually path variables. -``request`` ``RequestInterface`` The HTTP request to respond to. -``response`` ``Response`` The HTTP response to send based on the request. -============ ==================== =================================================================== - -HTTP Verbs ----------- - -Most of the action takes place inside the methods called in response to specific HTTP verbs. For example, to handle a ``GET`` request, implement the ``get`` method. - -.. code-block:: php - - class CatsCollectionHandler extends \pjdietz\WellRESTed\Handler - { - protected function get() - { - // Read some cats from storage. - // ...read these an array as the variable $cats. - - // Set the values for the instance's response member. This is what the - // Router will eventually output to the client. - $this->response->setStatusCode(200); - $this->response->setHeader("Content-Type", "application/json"); - $this->response->setBody(json_encode($cats)); - } - } - -Implement the methods that you want to support. If you don't want to support ``POST``, don't implement it. The default behavior is to respond with ``405 Method Not Allowed`` for most verbs. - -The methods available to implement are: - -=========== =========== =========================================== -HTTP Verb Method Default Behavior -=========== =========== =========================================== -``GET`` ``get`` 405 Method Not Allowed -``HEAD`` ``head`` Call ``get``, then clean the response body -``POST`` ``post`` 405 Method Not Allowed -``PUT`` ``put`` 405 Method Not Allowed -``DELETE`` ``delete`` 405 Method Not Allowed -``PATCH`` ``patch`` 405 Method Not Allowed -``OPTIONS`` ``options`` Add ``Allow`` header, if able -=========== =========== =========================================== - -OPTIONS and Allow ------------------ - -If you wish to provide a list of verbs that the endpoint supports, you can do this by redefining ``getAllowedMethods`` and returning an array of verbs. The handler will use this list to provide an ``Allow`` header when responding to ``OPTIONS`` requests or to requests for verbs the handler does not allow. - -.. code-block:: php - - protected function getAllowedMethods() - { - return ["GET", "HEAD", "POST", "OPTIONS"]; - } - -An ``OPTIONS`` request handled by this handler will now get a response similar to this: - -.. code-block:: http - - HTTP/1.1 200 OK - Allow: GET, HEAD, POST, OPTIONS - -A ``POST`` request's response will look like this: - -.. code-block:: http - - HTTP/1.1 405 Method Not Allowed - Allow: GET, HEAD, POST, OPTIONS - -.. _Dependency Injection: dependency-injection.html -.. _TemplateRoute: routes.html#template-routes -.. _RegexRoute: routes.html#regex-routes -.. _Routers: router.html diff --git a/docs/source/index.rst b/docs/source/index.rst index 420f48e..fed70b7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,17 +1,115 @@ WellRESTed ========== -WellRESTed is a micro-framework for creating RESTful Web services in PHP. It provides a lightweight yet powerful routing system and classes to make working with HTTP requests and responses clean and easy. +WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful middleware system, and a flexible router. -Contents: +Features +-------- + +PSR-7 HTTP Messages +^^^^^^^^^^^^^^^^^^^ + +Request and response messages are built to the interfaces standardized by PSR-7_ making it easy to share code and use components from other libraries and frameworks. + +The message abstractions facilitate working with message headers, status codes, variables extracted from the path, message bodies, and all the other aspects of requests and responses. + +Middleware +^^^^^^^^^^ + +The middleware_ system allows you to map build sequences of modular code that propagate from one to the next. For example, an authenticator can validate a request and forward it to a cache; the cache can check for a stored representation and forward to another middleware if no cached representation is found, etc. All of this happens without any one middleware needing to know anything about where it is in the chain or which middleware comes before or after. + +Most middleware is never autoloaded or instantiated until it is needed, so a Web service with hundreds of middleware still only creates instances required for the current request-respose cycle. + +You can register middleware directly, register callables that return middleware (e.g., dependency container services), or register strings containing the middleware classnames to autoload and instantiate on demand. + +Router +^^^^^^ + +The router_ allows you to define your endpoints using `URI Templates`_ like ``/foo/{bar}/{baz}`` that match patterns of paths and provide captured variables. You can also match exact paths for extra speed or regular expressions for extra flexibility. + +WellRESTed's automates responding to ``OPTIONS`` requests for each endpoint based on the method you assign. ``405 Method Not Allowed`` come free of charge as well for any methods you have not implemented on a given endpoint. + +Extensible +^^^^^^^^^^ + +All classes are coded to interfaces to allow you to provide your own implementations and use them in place of the built-in classes. For example, if your Web service needs to be able to dispatch middleware that implements a different interface, you can provide your own custom ``DispatcherInterface`` implentation. + +Example +------- + +Here's a customary "Hello, world!" example. This site will respond to requests for ``GET /hello`` with "Hello, world!" and provide custom responses for other paths (e.g., ``GET /hello/Molly`` will respond "Hello, Molly!"). + +The site will also provide an ``X-example: hello world`` using dedicated middleware, just to illustrate how middleware propagates. + +.. code-block:: php + + getAttribute("name", "world"); + + // Set the response body to the greeting and the status code to 200 OK. + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new Stream("Hello, $name!")); + + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); + + }; + + // Add a header to the response. + $headerAdder = function ($request, $response, $next) { + // Add the header. + $response = $response->withHeader("X-example", "hello world"); + // Propagate to the next middleware, if any, and return the response. + return $next($request, $response); + }; + + // Create a server + $server = new Server(); + + // Start each request-response cycle by dispatching the header adder. + $server->add($headerAdder); + + // The header adder will propagate to this router, which will dispatch the + // $hello middleware, possibly with a {name} variable. + $server->add($server->createRouter() + ->register("GET", "/hello", $hello) + ->register("GET", "/hello/{name}", $hello) + ); + + // Read the request from the client, dispatch middleware, and output. + $server->respond(); + +Contents +-------- .. toctree:: :maxdepth: 4 overview getting-started + middleware router - routes - handlers + uri-templates dependency-injection web-server-configuration + +.. _PSR-7: http://www.php-fig.org/psr/psr-7/ +.. _middleware: middleware.html +.. _router: router.html +.. _URI Templates: uri-templates.html diff --git a/docs/source/middleware.rst b/docs/source/middleware.rst new file mode 100644 index 0000000..d25810a --- /dev/null +++ b/docs/source/middleware.rst @@ -0,0 +1,295 @@ +Middleware +========== + +Okay, so what exactly **is** middleware? It's a nebulous term, and it's a bit reminscent of the Underpants gnomes. + +- Phase 1: Request +- Phase 2: ??? +- Phase 3: Respose + +Middleware is indeed Phase 2. It's something (a callable or object) that takes a request and a response as inputs, does something with the response, and sends the altered response back out. + +A Web service can built from many, many pieces of middleware, with each piece managing a specific task such as authentication or parsing representations. When each middleware runs, it is responsible for propagating the request through to the next middleware in the sequence—or deciding not to. + +So what's it look like? In essence, a single piece of middleware looks something like this: + +.. code-block:: php + + function ($request, $response, $next) { + + // Update the response. + /* $response = ... */ + + // Determine if any other middleware should be called after this. + if (/* Stop now without calling more middleware? */) { + // Return the response without calling any other middleware. + return $response; + } + + // Let the next middleware work on the response. This propagates "up" + // the chain of middleware, and will eventually return a response. + $response = $next($request, $response); + + // Possibly update the response some more. + /* $response = ... */ + + // Return the response. + return $response; + + } + +Defining Middleware +^^^^^^^^^^^^^^^^^^^ + +Middleware can be a callable (as in the `Getting Started`_) or an implementation of the ``WellRESTed\MiddlewareInterface`` (which implements ``__invoke`` so is technically a callable, too). + +.. rubric:: Callable + +.. code-block:: php + + /** + * @param Psr\Http\Message\ServerRequestInterface $request + * @param Psr\Http\Message\ResponseInterface $response + * @param callable $next + * @return Psr\Http\Message\ResponseInterface + */ + function ($request, $response, $next) { } + +.. rubric:: MiddlewareInterface + +.. code-block:: php + + register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler'); + +The class is not loaded, and no instances are created, until the route is matched and dispatched. Even for a router with 100 routes, no middleware registered by string name is loaded, except for the one that matches the request. + +Callable Provider +----------------- + +You can also use a callable to instantiate and return a ``MiddlewareInterface`` instance or middleware callable. + +.. code-block:: php + + $router->add("GET,PUT,DELETE", "/widgets/{id}", function () { + return new \Webservice\Widgets\WidgetHandler(); + }); + +This still delays instantiation, but gives you some added flexibility. For example, you could define middleware that receives some configuration upon construction. + +.. code-block:: php + + $container = new MySuperCoolDependencyContainer(); + + $router->add("GET,PUT,DELETE", "/widgets/{id}", function () use ($container) { + return new \Webservice\Widgets\WidgetHandler($container); + }); + +This is one approach to `dependency injection`_. + +Middleware Callable +------------------- + +Use a middleware callable directly. + +.. code-block:: php + + $router->add("GET,PUT,DELETE", "/widgets/{id}", function ($request, $response, $next) { + $response = $response->withStatus(200) + ->withHeader("Content-type", "text/plain") + ->withBody(new \WellRESTed\Message\Stream("It's a bunch of widgets!"); + return $next($request, $response); + }); + +Instance +-------- + +You can also provide pass an instnace directly as middleware. + +.. code-block:: php + + $router->add("GET,PUT,DELETE", "/widgets/{id}", new \Webservice\Widgets\WidgetHandler()); + +.. warning:: + + This is simple, but has a significant disadvantage over the other options because each middleware used this way will be loaded and instantiated, even though only one middleware will actually be used for a given request-response cycle. You may find this approach useful for testing, but avoid if for production code. + +Array +----- + +Why use one middleware when you can use more? + +Provide a sequence of middleware as an array. Each component of the array can be any of the varieties listed in this section. + +When disptached, the middleware in the array will run in order, with each calling the one following via the ``$next`` parameter. + +.. code-block:: php + + $router->add("GET", "/widgets/{id}", ['Webservice\Auth', $jsonParser, $widgetHandler]); + +Chaining Middleware +^^^^^^^^^^^^^^^^^^^ + +Chaining middleware together allows you to build your Web service in a discrete, modular pieces. Each middleware in the chain makes the decision to either move the request up the chain by calling ``$next``, or stop propagation by returning a response without calling ``$next``. + +Propagating Up the Chain +------------------------ + +Imagine we want to add authorization to the ``/widgets/{id}`` endpoint. We can do this without altering the existing middleware that deals with the widget itself. + +What we do is create an additional middleware that performs just the authorization task. This middleware will inspect the incoming request for authorization headers, and either move the request on up the chain to the next middleware if all looks good, or send a request back out with an appropriate status code. + +Here's an example authorization middleware using pseudocode. + +.. code-block:: php + + namespace Webserice; + + class Authorization implements \WellRESTed\MiddlewareInterface + { + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + // Validate the headers in the request. + try { + $validateUser($request); + } catch (InvalidHeaderException $e) { + // User did not supply the right headers. + // Respond with a 401 Unauthorized status. + return $response->withStatus(401); + } catch (BadUserException $e) { + // User is not permitted to access this resource. + // Respond with a 403 Forbidden status. + return $response->withStatus(403); + } + + // No exception was thrown, so propagate to the next middleware. + return $next($request, $response); + } + } + +We can add authorization for just the ``/widgets/{id}`` endpoint like this: + +.. code-block:: php + + $router->register("GET,PUT,DELETE", "/widgets/{id}", [ + 'Webservice\Auhtorizaiton', + 'Webservice\Widgets\WidgetHandler' + ]); + +Or, if you wanted to use the authorization for the entire service, you can add it to the ``Server`` in front of the ``Router``. + + .. code-block:: php + + $server = new \WellRESTed\Server(); + $server + ->add('Webservice\Auhtorizaiton') + ->add($server->createRouter() + ->register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler') + ) + ->respond(); + +Moving Back Down the Chain +-------------------------- + +The authorization example returned ``$next($request, $response)`` immidiately, but you can do some interesting things by working with the response that comes back from ``$next``. Think of the request as taking a round trip on the subway with each middleware being a stop along the way. Each of the stops you go through going up the chain, you also go through on the way back down. + +We could add a caching middleware in front of ``GET`` requests for a specific widget. This middleware will check if a cached representation exists for the resource the client requested. If it exists, it will send it out to the client without ever bothering the ``WidgetHandler``. If there's no representation cached, it will call ``$next`` to propgate the request up the chain. On the return trip (when the call to ``$next`` finishes), the caching middleware will inspect the response and store the body to the cache for next time. + +Here's a pseudocode example: + +.. code-block:: php + + namespace Webserice; + + class Cache implements \WellRESTed\MiddlewareInterface + { + public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next) + { + // Inspect the request path to see if there is a representation on + // hand for this resource. + $representation = $this->getCachedRepresentation($request); + if ($representation !== null) { + // There is already a cached representation. Send it out + // without propagating. + return $reponse + ->withStatus(200) + ->withBody($representation); + } + + // No representation exists. Propagate to the next middleware. + $response = $next($request, $response); + + // Attempt to store the response to the cache. + $this->storeRepresentationToCache($response); + + return $response; + } + + private function getCachedRepresentation(ServerRequestInterface $request) + { + // Look for a cached representation. Return null if not found. + // ... + } + + private function storeRepresentationToCache(ResponseInterface $response) + { + // Ensure the response contains a success code, a valid body, + // headers that allow caching, etc. and store the represetnation. + // ... + } + } + +We can add this caching middleware in the chain between the authorization middleware and the Widget. + +.. code-block:: php + + $router->register("GET,PUT,DELETE", "/widgets/{id}", [ + 'Webservice\Auhtorizaiton', + 'Webservice\Cache', + 'Webservice\Widgets\WidgetHandler' + ]); + +Or, if you wanted to use the authorization and caching middelware for the entire service, you can add them to the ``Server`` in front of the ``Router``. + +.. code-block:: php + + $server = new \WellRESTed\Server(); + $server + ->add('Webservice\Auhtorizaiton') + ->add('Webservice\Cache') + ->add($server->createRouter() + ->register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler') + ) + ->respond(); + +.. _Dependency Injection: dependency-injection.html +.. _Getting Started: getting-started.html diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 3a20329..37d0f41 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -10,20 +10,14 @@ The recommended method for installing WellRESTed is to use the PHP dependency ma { "require": { - "pjdietz/wellrested": "~2.3" + "wellrested/wellrested": "~3.0" } } Requirements ^^^^^^^^^^^^ -- PHP 5.3.0 -- `PHP cURL`_ for making requests with the ``Client`` class (Optional) - -.. note:: - - While WellRESTed will work on PHP 5.3.0, the unit tests require 5.4.0 as - some make use of the PHP built-in web server. +- PHP 5.4.0 License ^^^^^^^ diff --git a/docs/source/router.rst b/docs/source/router.rst index 9025f67..c553255 100644 --- a/docs/source/router.rst +++ b/docs/source/router.rst @@ -1,341 +1,360 @@ 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 router is a type of middleware_ that organizes the components of a site by associating URI paths with other middleware_. When the router receives a request, it examines the path components of the request's URI, determines which "route" matches, and dispatches the associated middleware_. The dispatched middleware_ 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: + +Basic Usage +^^^^^^^^^^^ + +Typically, you will want to use the ``WellRESTed\Server::createRouter`` method to create a ``Router``. .. code-block:: php - createRouter(); - 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``. +Suppose ``$catHandler`` is a middleware that you want to dispatch whenever a client makes a ``GET`` request to the path ``/cats/``. Use the ``register`` method map it to that path and method. .. code-block:: php - $router->add("/", "\\MyApi\\RootHandler"); + $router->register("GET", "/cats/", $catHandler); -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_. +The ``register`` method is fluid, so you can add multiple routes in either of these styles: .. code-block:: php - $router->add( - ["/", "\\MyApi\\RootHandler"], - ["/cats/", "\\MyApi\\CatHandler"], - ["/dogs/*", "\\MyApi\\DogHandler"], - ["/guinea-pigs/{id}", "\\MyApi\\GuineaPigHandler"], - ["~/hamsters/([0-9]+)~", "\\MyApi\\HamsterHandler"] - ); + $router->register("GET", "/cats/", $catReader); + $router->register("POST", "/cats/", $catWriter); + $router->register("GET", "/cats/{id}", $catItemReader); + $router->register("PUT,DELETE", "/cats/{id}", $catItemWriter); + +...Or... + +.. code-block:: php + + $router + ->register("GET", "/cats/", $catReader) + ->register("POST", "/cats/", $catWriter) + ->register("GET", "/cats/{id}", $catItemReader) + ->register("PUT,DELETE", "/cats/{id}", $catItemWriter); + +Paths +^^^^^ + +A router can map middleware to an exact path, or to a pattern of paths. + +Static Routes +------------- + +The simplest type of route is called a "static route". It maps middleware to an exact path. + +.. code-block:: php + + $router->register("GET", "/cats/", $catHandler); + +This route will map a request to ``/cats/`` and only ``/cats/``. It will **not** match requests to ``/cats`` or ``/cats/molly``. + +Prefix Routes +------------- + +The next simplest type of route is a "prefix route". A prefix route matches requests by the beginning of the path. + +To create a "prefix handler", include ``*`` at the end of the path. For example, this route will match any request that begins with ``/cats/``. + +.. code-block:: php + + $router->register("GET", "/cats/*", $catHandler); + +Template Routes +--------------- + +Template routes allow you to provide patterns for paths with one or more variables (sections surrounded by curly braces) that will be extracted. + +For example, this template will match requests to ``/cats/12``, ``/cats/molly``, etc., + +.. code-block:: php + + $router->register("GET", "/cats/{cat}", $catHandler); + +When the router dispatches a route matched by a template route, it provides the extracted variables as an associative array. To access a variable, call the request object's ``getAttribute`` method method and pass the variable's name. + +For a request to ``/cats/molly``: + +.. code-block:: php + + $catHandler = function ($request, $response, $next) { + $name = $request->getAttribute("cat"); + // molly + ... + } + +Template routes are very powerful, and this only scratches the surface. See `URI Templates`_ for a full explaination of the syntax supported. + +Regex Routes +------------ + +You can also use regular expressions to describe route paths. + +.. code-block:: php + + $router->register("GET", "~cats/(?[a-z]+)-(?[0-9]+)~", $catHandler); + +When using regular expression routes, the attributes will contain the captures from preg_match_. + +For a request to ``/cats/molly-90``: + +.. code-block:: php + + $catHandler = function ($request, $response, $next) { + $vars = $request->getAttributes(); + /* + Array + ( + [0] => cats/molly-12 + [name] => molly + [1] => molly + [number] => 12 + [2] => 12 + ... Plus any other attributes that were set ... + ) + */ + ... + } + +Route Priority +-------------- + +A router will often contain many routes, and sometimes more than one route will match for a given request. When the router looks for a matching route, it performs these checks: + +#. If there is a static route with exact match to path, dispatch it. +#. If one prefix route matches the beginning of the path, disptach it. +#. If multiple prefix routes match, dispatch the longest matching prefix route. +#. Inspect each pattern route (template and regular expression) in the order added. Dispatch the first route that matches. +#. If no pattern routes match, return a reponse with a ``404 Not Found`` status. + +Static vs. Prefix +~~~~~~~~~~~~~~~~~ + +Consider these routes: + +.. code-block:: php + + $router + ->register("GET", "/cats/", $static); + ->register("GET", "/cats/*", $prefix); + +The router will dispatch a request for ``/cats/`` to ``$static`` because the static route ``/cats/`` has priority over the prefix route ``/cats/*``. + +The router will dispatch a request to ``/cats/maine-coon`` to ``$prefix`` because it is not an exact match for ``/cats/``, but it does begin with ``/cats/``. + +Prefix vs. Prefix +~~~~~~~~~~~~~~~~~ + +Given these routes: + +.. code-block:: php + + $router + ->register("GET", "/dogs/*", $short); + ->register("GET", "/dogs/sporting/*", $long); + +A request to ``/dogs/herding/australian-shepherd`` will be dispatched to ``$short`` because it matches ``/dogs/*``, but does not match ``/dogs/sporting/*`` + +A request to ``/dogs/sporing/flat-coated-retriever`` will be dispatched to ``$long`` because it matches both routes, but ``/dogs/sporting`` is longer. + +Prefix vs. Pattern +~~~~~~~~~~~~~~~~~~ + +Given these routes: + +.. code-block:: php + + $router + ->register("GET", "/dogs/*", $prefix); + ->register("GET", "/dogs/{group}/{breed}", $pattern); + +``$pattern`` will never be dispatched because any route that matches ``/dogs/{group}/{breed}`` also matches ``/dogs/*``, and prefix routes have priority over pattern routes. + +Pattern vs. Pattern +~~~~~~~~~~~~~~~~~~~ + +When multiple pattern routes match a path, the first one that was added to the router will be the one disptached. Be careful to add the specific routes before the general routes. For example, say you want to send traffic to two similar looking URIs to different middleware based whether the variables were supplied as numbers or letters—``/dogs/102/132`` should be dispatched to ``$numbers``, while ``/dogs/herding/australian-shepherd`` should be dispatched to ``$letters``. + +This will work: + +.. code-block:: php + + // Matches only when the variables are digits. + $router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers); + // Matches variables with any unreserved characters. + $router->register("GET", "/dogs/{group}/{breed}", $letters); + +This will **NOT** work: + +.. code-block:: php + + // Matches variables with any unreserved characters. + $router->register("GET", "/dogs/{group}/{breed}", $letters); + // Matches only when the variables are digits. + $router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers); + +This is because ``/dogs/{group}/{breed}`` will match both ``/dogs/102/132`` and ``/dogs/herding/australian-shepherd``. If it is added to the router before the route for ``$numbers``, it will be dispatched before the route for ``$numbers`` is ever evaluated. + +Methods +^^^^^^^ + +When you register a route, you can provide a specific method, a list of methods, or a wildcard to indcate any method. + +Registering by Method +--------------------- + +Specify a specific middleware for a path and method by including the method as the first parameter. + +.. code-block:: php + + // Dispatch $dogCollectionReader for GET requests to /dogs/ + $router->register("GET", "/dogs/", $dogCollectionReader); + + // Dispatch $dogCollectionWriter for POST requests to /dogs/ + $router->register("POST", "/dogs/", $dogCollectionWriter); + +Registering by Method List +-------------------------- + +Specify the same middleware for multiple methods for a given path by proving a comma-separated list of methods as the first parameter. + +.. code-block:: php + + // Dispatch $catCollectionHandler for GET and POST requests to /cats/ + $router->register("GET,POST", "/cats/", $catCollectionHandler); + + // Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc. + $router->register("GET", "/cats/{id}", $catItemReader); + + // Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc. + $router->register("PUT,DELETE", "/cats/{id}", $catItemWriter); + +Registering by Wildcard +----------------------- + +Specify middleware for all methods for a given path by proving a ``*`` wildcard. + +.. code-block:: php + + // Dispatch $guineaPigHandler for all requests to /guinea-pigs/, regardless of method. + $router->register("*", "/guinea-pigs/", $guineaPigHandler); + + // Use $hamstersHandler by default for requests to /hamsters/ + $router->register("*", "/hamsters/", $hamstersHandler); + + // Provide a specific handler for POST /hamsters/ + $router->register("POST", "/hamsters/", $hamstersPostOnly); .. 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. + The wildcard ``*`` can be useful, but be aware that the associated middleware will need to manage ``HEAD`` and ``OPTIONS`` requests, whereas this is done automatcially for non-wildcard routes. -Specifying Handlers -^^^^^^^^^^^^^^^^^^^ +HEAD +---- -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. +Any route that supports ``GET`` requests will automatically support ``HEAD``. You don't need to provide any specific middleware for ``HEAD``, and you usually shouldn't. (Although you can if you want.) -.. _error handlers: `Error Handling`_ +For most cases, just implement ``GET``, and the webserver will manage suppressing the response body for you. -Fully Qualified Class Name (FQCN) ---------------------------------- +OPTIONS, 405 Responses, and Allow Headers +----------------------------------------- -Specify a class by FQCN by passing a string as the second parameter. +When you add routes to a router by method, the router automatically provides responses for ``OPTIONS`` requests. For example, given this route: .. code-block:: php - $router->add("/cats/{id}", "\\MyApi\\CatHandler"); + // Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc. + $router->register("GET", "/cats/{id}", $catItemReader); -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. + // Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc. + $router->register("PUT,DELETE", "/cats/{id}", $catItemWriter); -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. +An ``OPTIONS`` request to ``/cats/12`` will provide a response like: + +.. code-block:: http + + HTTP/1.1 200 OK + Allow: GET,PUT,DELETE,HEAD,OPTIONS + +Likewise, a request to an unsupport method will return a ``405 Method Not Allowed`` response with a descriptive ``Allow`` header. + +A ``POST`` request to ``/cats/12`` will provide: + +.. code-block:: http + + HTTP/1.1 405 Method Not Allowed + Allow: GET,PUT,DELETE,HEAD,OPTIONS -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 +Error Responses ^^^^^^^^^^^^^^^ -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. +When a router is unable to dispatch a route because either the path or method does not match a defined route, it will provide an appropriate error response code—either ``404 Not Found`` or ``405 Method Not Allowed``. + +The router always checks the path first. If route for that path matches, the router responds ``404 Not Found``. + +If the router is able to locate a route that matches the path, but that route doesn't support the request's method, the router will respond ``405 Method Not Allowed``. + +Given this router: .. code-block:: php - $router->add("/cats/{catId}", function ($rqst, $args) { + $router + ->register("GET", "/cats/", $catReader) + ->register("POST", "/cats/", $catWriter) + ->register("GET", "/dogs/", $catItemReader) - // Find a cat in the cat repository. - $catProvider = new CatProvider(); - $cat = $catProvider->getCatById($args["catId"); +The following requests wil provide these responses: - // Throw a NotFoundException if $cat is null. - if (is_null($cat)) { - throw new \pjdietz\WellRESTed\Exceptions\HttpExceptions\NotFoundException(); - } +====== ========== ======== +Method Path Response +====== ========== ======== +GET /hamsters/ 404 Not Found +PUT /cats/ 405 Method Not Allowed +====== ========== ======== - // Do cat stuff and return a response... - // ... +.. note:: - }); - -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); + When the router fails to dispatch a route, or when it responds to an ``OPTIONS`` request, is will stop propagation, and any middleware that comes after the router will not be dispatched. 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. +For large Web services with large numbers of endpoints, a single, monolithic router may not to optimal. To avoid having each request test every pattern-based route, you can break up a router into subrouters. -Using Router Subclasses ------------------------ +This works because a ``Router`` is type of middleware, and can be used wherever middleware can be used. -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``. +Here's an example where all of the traffic beginning with ``/cats/`` is sent to one router, and all the traffic for endpoints beginning with ``/dogs/`` is sent to another. .. code-block:: php - $router = new \pjdietz\WellRESTed\Router(); - $router->add( - ["/cats/*", "\\MyApi\\CatRouter"], - ["/dogs/*", "\\MyApi\\DogRouter"] + $server = new Server(); + + $catRouter = $server->createRouter() + ->register("GET", "/cats/", $catReader) + ->register("POST", "/cats/", $catWriter) + // ... many more endpoints starting with /cats/ + ->register("POST", "/cats/{cat}/photo/{gallery}/{width}x{height}.{extension}", $catImageHandler); + + $dogRouter = $server->createRouter() + ->register("GET,POST", "/dogs/", $dogHandler) + // ... many more endpoints starting with /dogs/ + ->register("POST", "/dogs/{dog}/photo/{gallery}/{width}x{height}.{extension}", $dogImageHandler); + + $server->add($server->createRouter() + ->register("*", "/cats/*", $catRouter) + ->register("*", "/dogs/*", $dogRouter) ); -Here are router subclasses that contain only routes beginning with the expected prefixes. + $server->respond(); -.. 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 +.. _preg_match: http://php.net/manual/en/function.preg-match.php +.. _URI Template: `URI Templates`_s +.. _URI Templates: uri-templates.html +.. _middleware: middleware.html diff --git a/docs/source/routes.rst b/docs/source/routes.rst deleted file mode 100644 index d83f3ff..0000000 --- a/docs/source/routes.rst +++ /dev/null @@ -1,189 +0,0 @@ -Routes -====== - -WellRESTed provides a number of types of routes to match a request's path to a handler_ that will provide a response. - -`Static Routes`_ - Match request paths exactly -`Prefix Routes`_ - Match beginnings of paths -`Template Routes`_ - Match against URI templates -`Regex Routes`_ - Match against regular expressions - -Static Routes -^^^^^^^^^^^^^ - -A static route maps to an exact path. The router will always check for a static route first before trying any other route types. - -The following will match all requests to ``/cats/``. - -.. code-block:: php - - $router->add("/cats/", $catHandler); - -A router can only have one static route for a given path, so adding a duplicate static route will replace the first. - -.. code-block:: php - - $router->add("/cats/", $catHandler); - $router->add("/cats/", $dogHandler); - // Requests for /cats/ will now be dispatched to $dogHandler. - -Prefix Routes -^^^^^^^^^^^^^ - -You can also create prefix routes which will match any path that *begins* with a given prefix. To create a prefix route, include an asterisk (``*``) at the end of the path. The following will match all requests beginning with ``/cats/``, including ``/cats/``, ``/cats/maine-coon``, ``/cats/persian``, etc. - -.. code-block:: php - - $router->add("/cats/*", $catHandler)); - -Prefix routes are evaluated after static routes. So, given this router… - -.. code-block:: php - - $router->add("/cats/", $catRootHandler); - $router->add("/cats/*", $catSubHandler); - -…requests for ``/cats/`` will be dispatched to ``$catRootHandler``, while ``/cats/maine-coon``, ``/cats/persian``, etc. will be dispatched to ``$catSubHandler``. - -Finding the Best Match ----------------------- - -In the event that multiple prefix routes match the request, the router uses the longest match to determine the best match. Give this router… - -.. code-block:: php - - $router->add("/dogs/*", $dogHandler); - $router->add("/dogs/sporting/*", $sportingHandler); - -…requests to ``/dogs/sporting/flat-coated-retriever`` will be dispatched to ``$sportingHandler``, while requests to ``/dogs/herding/boarder-collie`` will be dispatched to ``$dogHandler``. - -Template Routes -^^^^^^^^^^^^^^^ - -Template routes allow the router to extract parts of the path and make them available to the handler_. To create a template route, include one or more variables in the path. - -.. code-block:: php - - $router->add("/dogs/{group}/{breed}", $dogHandler); - -When the router gets a request for ``/dogs/herding/australian-shepherd``, it will dispatch ``$dogHandler`` and pass this array: - -.. code-block:: php - - [ - "group" => "herding", - "breed" => "australian-shepherd" - ] - -The name of each variable (the part between the curly braces) becomes an array key while the extracted text becomes the corresponding value. - -Default Variable Pattern ------------------------- - -By default, the variables will match digits, upper- and lowercase letters, hyphens, and underscores. To change this, pass a partial regular expression to ``Router::add`` as the third parameter. - -The following will restrict the route to match only when the variable matches digits. - -.. code-block:: php - - $router->add("/cats/{catId}", $catHandler, "[0-9]+"); - -Variable Pattern Map --------------------- - -For more specificity, you can provide an array mapping variables to their patterns as the fourth parameter. (The third parameter will server as the default for any variables not included in the map). - -This will restrict ``{dogId}`` to be digits, while leaving ``{group}`` and ``{breed}`` to be matched by the default. - -.. code-block:: php - - $router->add("/dogs/{group}/{breed}/{dogId}", $dogHandler, null, [ - "dogId" => "[0-9]+" - ]); - -Pattern Constants ------------------ - -The ``TemplateRoute`` class provides a few of these patterns as class constants. - -=============== ===================== =========== -Constant Regex Description -=============== ===================== =========== -``RE_SLUG`` ``[0-9a-zA-Z\-_]+`` **(Default)** "URL-friendly" characters -``RE_NUM`` ``[0-9]+`` Digits only -``RE_ALPHA`` ``[a-zA-Z]+`` Letters only -``RE_ALPHANUM`` ``[0-9a-zA-Z]+`` Letters and digits -=============== ===================== =========== - -You can use the class constants or provide your own strings. - -This will restrict ``{dogId}`` to be digits, and restrict ``{group}`` and ``{breed}`` to lowercase letters and hyphens. - -.. code-block:: php - - $router->add("/dogs/{group}/{breed}/{dogId}", $dogHandler, "[a-z\-]", [ - "dogId" => TemplateRoute::RE_NUM, - ]); - - -Regex Routes -^^^^^^^^^^^^ - -If template routes don't give you enough control, you can make a route that matches a regular expression. - -.. code-block:: php - - $router->add("~/cat/[0-9]+~", $catHandler); - -This will match ``/cat/102``, ``/cat/999``, etc. - -Capture Groups --------------- - -To make this more useful, add a capture group. A regex route makes captures available to the dispatched handler the same way template route makes matched variables available. - -So this route… - -.. code-block:: php - - $router->add("~/cat/([0-9]+)~", $catHandler); - -…with the path ``/cat/99``, creates this array of matches: - -.. code-block:: php - - [ - 0 => "/cat/99", - 1 => "99" - ] - -(Note that the entire matched path will always be the ``0`` item, and captured groups will begin at ``1``.) - -You can also used named capture groups like this: - -.. code-block:: php - - $router->add("~/cat/(?[0-9]+)~", $catHandler); - -The path ``/cat/99`` creates this array of matches: - -.. code-block:: php - - [ - 0 => "/cat/99", - 1 => "99", - "catId" => "99" - ] - -Delimiter ---------- - -To prevent the route from interpreting your regular expression as a really weird path, use a character other than `/` as the regular expression delimiter_. For example, `~` or `#`. - -.. _delimiter: http://php.net/manual/en/regexp.reference.delimiters.php -.. _handler: Handlers_ -.. _Handlers: handlers.html diff --git a/docs/source/uri-templates.rst b/docs/source/uri-templates.rst new file mode 100644 index 0000000..a12fd6e --- /dev/null +++ b/docs/source/uri-templates.rst @@ -0,0 +1,181 @@ +URI Templates +============= + +WellRESTed allows you to register middleware with a router using URI Templates. These templates include variables (enclosed in curly braces) which are extracted and made availble to the disptached middleware. + +The URI Template syntax is based on the URI Templates defined in `RFC 6570`_. + +Example +------- + +Register middleware with a URI Template by providing a path that include at least one section enclosed in curly braces. The curly braces define variables for the template. + +.. code-block:: php + + $router->register("GET", "/widgets/{id}", $widgetHandler); + +The router will match requests for paths like ``/widgets/12`` and ``/widgets/mega-widget`` and dispatch ``$widgetHandler`` with the extracted variables made available as request attributes. + +To read a path variable, the ``$widgetHandler`` middleware inspects the request attribute named "id". + +.. code-block:: php + + $widgetHandler = function ($request, $response, $next) { + + // Read the variable extracted form the path. + $id = $request->getAttribute("id"); + // "12" + + }; + +Syntax +------ + +To illustrate the syntax for various matches, we'll start by providing table of variable names and values that will be used throughout the examples. + +.. class:: code-table +.. list-table:: Variable names and values + :header-rows: 1 + + * - Name + - Value + * - empty + - "" + * - half + - "50%" + * - hello + - "Hello, World!" + * - path + - "/foo/bar" + * - var + - "value" + * - who + - "fred" + * - x + - "1024" + * - y + - "768" + * - count + - ["one", "two", "three"] + * - list + - ["red", "green", "blue"] + +Simple Strings +############## + +Variables by default match any unreserved characters. This includes all alpha-numeric characters, plus ``-``, ``.``, ``_``, and ``~``. All other characers MUST be percent encoded. + +.. class:: code-table +.. list-table:: Simple Strings + :header-rows: 1 + + * - URI Template + - Request Path + * - /{var} + - /value + * - /{hello} + - /Hello%20World%21 + * - /{x,hello,y} + - /1024,Hello%20World%21,768 + +Reserver Charaters +################## + +To match reserved character, begin the expression with a ``+``. + +.. class:: code-table +.. list-table:: Reserved Characters + :header-rows: 1 + + * - URI Template + - Request Path + * - /{+var} + - /value + * - /{+hello} + - /Hello%20World! + * - {+path}/here + - /foo/bar/here + +Dot Prefix +########## + +Expressions beginning with ``.`` indicate that each variable matched begins with ``.``. + +.. class:: code-table +.. list-table:: Dot Prefix + :header-rows: 1 + + * - URI Template + - Request Path + * - /{.who} + - /.fred + * - /{.half,who} + - /.50%25.fred + * - /X{.empty} + - /X. + +Path Segments +############# + +Expressions beginning with ``/`` indicate that each variable matched beings with ``/``. + +.. class:: code-table +.. list-table:: Path Segments + :header-rows: 1 + + * - URI Template + - Request Path + * - {/who} + - /fred + * - {/half,who} + - /50%25/fred + * - {/var,empty} + - /value/ + +Explosion +######### + +The explosion operator (``*``) at the end of an expression indicates that the extracted value is a list, and will be provided to the middleware as an array. + +.. class:: code-table +.. list-table:: Explosion + :header-rows: 1 + + * - URI Template + - Request Path + * - /{count*} + - /one,two,three + * - {/count*} + - /one/two/three + * - X{.list*} + - X.red.green.blue + +Limitations +----------- + +While WellRESTed's URI templates are modeled after `RFC 6570`_, there are some parts of the RFC that WellRESTed does not implement. Some of these are because WellRESTed's uses a URI template and a given path to extract variables, whereas `RFC 6570`_ describes using a URI template and variables to create a path. Other parts are just not implemented yet, but may be in future releases. + +Query and Fragment +################## + +Anything relating to the query or fragment is omitted. This is because routing is based only on the path component of the request's URI and does not make use of the query or fragment for routing purposes. Furthur, a request for a valid resource with an invalid query **should** generally result in a ``400 Bad Request`` response, not a ``404 Not Found`` response, but taking the query into account for routing would make the ``404`` response happen automatically. A developer may have reason to respond ``404`` based on the query, but this should not be the library's default behavior. + +Path-Style Parameter Expansion +############################## + +`RFC 6570 Section 3.2.7`_ describes "path-style parameter expansion" where semi-colon-prefixed expressions (e.g., ``{;x,y}``) expand to key-value pairs (e.g., ``;x=1024;y=768``). This is not currently implemented in WellRESTed, although later releases may provide this functionality. + +Variable Names +############## + +Variable names MUST contain only alphanumeric characters and ``_``. + +Unique Variable Names +##################### + +Variables names within a given URI Template MUST be unique. + +Although some examples in `RFC 6570`_ include the same variable name multiple times, this is not supported by WellRESTed. + +.. _RFC 6570: https://tools.ietf.org/html/rfc6570 +.. _RFC 6570 Section 3.2.7: https://tools.ietf.org/html/rfc6570#section-3.2.7 diff --git a/docs/source/web-server-configuration.rst b/docs/source/web-server-configuration.rst index accda0c..61e6d0a 100644 --- a/docs/source/web-server-configuration.rst +++ b/docs/source/web-server-configuration.rst @@ -1,7 +1,7 @@ Web Server Configuration ======================== -You will typically want to have all traffic on your site directed to a single script that dispatches the router. Here are basic setups for doing this in Nginx_ and Apache_. +You will typically want to have all traffic on your site directed to a single script that creates a ``WellRESTed\Server`` and calls ``respond``. Here are basic setups for doing this in Nginx_ and Apache_. Nginx ^^^^^ @@ -31,7 +31,6 @@ Nginx } - Apache ^^^^^^