Request Lifecycle - rawebone/Wilson GitHub Wiki

Wilson works by intercepting HTTP Requests, routes that request to an appropriate Controller and then sends a Response back to the client. Lets start at the start with the index.php file we created in Installation and Setup:

<?php

require_once "vendor/autoload.php"

$api = new Wilson\Api();
$api->dispatch();

Here we create an instance of an Api object, which is the entry point for the framework. The call we make to dispatch tells the framework to intercept the request and send the response. Neat! So that leaves us with Controllers.

Controllers

A Controller is a method of an object which processes a Request and prepares a Response. The object which contains Controllers is referred to as a Resource, but we'll come to that later. Say we have a series of users that we want to expose through our API, we would write a Controller such as:


use Wilson\Http\Request;
use Wilson\Http\Response;

class Users
{
    /**
     * @route GET /users/
     */
    function getUsers(Request $request, Response $response)
    {
        $data = $this->magicallyReturnData();
        
        $response->setStatus(200);
        $response->setHeader("Content-Type", "application/json");
        $response->setBody(json_encode($data));
    }
}

We then have to tell Wilson to consider the User object when dispatching the request:

<?php

require_once "vendor/autoload.php";

$api = new Wilson\Api();
$api->resources = array("Users");
$api->dispatch();

Now, if we were to spool up a configured web server and go to, say, http://localhost/users/ we would then expect to see some JSON output. The framework looks at the request and sees that the query string is /users and that the HTTP method is GET. It looks over the annotations that are supplied in the exposed resources and sees that getUsers can handle this type of request. So it creates an instance of the Users object and calls the getUsers method, passing it a request and response object. Once the call to getUsers completes the response is then prepared and sent back to the User Agent.

The framework creates an instance of the Users object because it minimises the amount of objects that are held in memory during the processing of the request which helps reduce your applications footprint. The knock on effect is that the Resource object cannot have any constructor arguments and as such any dependencies have to be used with the Service Container.

So far so simples. However, we now want to return a single user through our API with a URI like /users/1 - how do we do this? Wilson provides the ability to capture values in URIs called Parameters:


use Wilson\Http\Request;
use Wilson\Http\Response;

class Users
{
    /**
     * @route GET /users/
     */
    function getUsers(Request $request, Response $response)
    {
        $data = $this->magicallyReturnData();
        
        $response->setStatus(200);
        $response->setHeader("Content-Type", "application/json");
        $response->setBody(json_encode($data));
    }
    
    /**
     * @route GET /users/{id}
     */
    function getUser(Request $request, Response $response)
    {
        $data = $this->magicallyReturnData($request->getParam("id"));
        
        // ...
    }
}

Cool! But not necessarily secure because id could be anything; as such the framework provides another annotation which can be used in conjunction with Parameters called Conditions that allows you more control over your input:


    /**
     * @route GET /users/{id}
     * @where id \d+
     */
    function getUser(Request $request, Response $response)
    {
        $data = $this->magicallyReturnData($request->getParam("id"));
        
        // ...
    }

We are now saying that the Parameter id must meet the regular expression of \d+ or in other words that id must be an integer of any length.

It is worth keeping in mind when developing an API that using integer based IDs can enable third parties to easily scrape your data and so should be used with caution.

Middleware

Often when working with an API you find yourself performing repetitive tasks such as authentication, content acceptance, et al. You might find yourself writing code like:


    /**
     * @route GET /users/{id}
     * @where id \d+
     */
    function getUser(Request $request, Response $response)
    {
        if (!$this->authenticate()) {
            $response->setStatus(401);
            return
        }
    
        $data = $this->magicallyReturnData($request->getParam("id"));
        
        // ...
    }

This violates DRY and distracts from the real problem your Controller is trying to solve. As such, Wilson provides a Middleware system that allows you easily reuse code. For example:


use Wilson\Http\Request;
use Wilson\Http\Response;

trait Middleware
{
    function authenticate(Request $request, Response $response)
    {
        if ($request->getUsername() === "John") {
            $response->setStatus(401);
            return false;
        }
    }
}


    use Middleware;

    /**
     * @route GET /users/{id}
     * @where id \d+
     * @through authenticate
     */
    function getUser(Request $request, Response $response)
    {
        $data = $this->magicallyReturnData($request->getParam("id"));
        
        // ...
    }

So what we have done here is create a trait to hold all of our Middleware, added a Middleware Controller called authenticate which returns boolean false if the request process should abort, and added a @through annotation to the getUsers Controller. Now when the framework dispatches the request it will put the request through the authenticate method first.

We can assign as much Middleware as we require and the framework will process each in turn until one returns boolean false or there are no more Controllers left. A Middleware Controller is exactly the same as a Controller, the only difference being that a Middleware Controller does not have a @route annotation.

Request and Response Headers

Please be aware that header names are not normalised between requests and responses. This is because normalising the names of headers in the $_SERVER superglobal can add a substantial amount of time to the request add so we instead use the SAPI appropriate names, i.e.:


$req->getHeader("HTTP_CONTENT_TYPE");
$resp->setHeader("Content-Type", "text/html");

Convenience Methods

Response Helper


$response = new Wilson\Http\Response();

// Configure the response in a single hit
$response->make("Something went wrong", 500, ["Content-Type" => "text/html"]);
$response->make("This is your content");
$response->make("Created!", 201);

// Send back JSON
$response->json(["error" => "error message"], 500);
$response->json("Custom JSON MIME Type", 200, ["Content-Type" => "application/vnd.io+json"]);

// Send back HTML
$response->html("<b>Hello!</b>");
$response->html("<b>Custom MIME Type", 200, ["Content-Type" => "text/xhtml"]);

// Stream response data. Useful for delaying action until
// the body of the request is being sent back
$response->setBody(function ()  use ($users)
{
    echo json_encode($users);
});

// Send back a HTTP valid datetime:
$response->setDateHeader("Last-Modified", new DateTime());

Caching

Caching is a working in progress, but there are basic provisions in place to help reduce the amount of work needed to be done in your application.

In general, Wilson allows you to defer processing until after the request has been validated as not being cached:


$response->whenCacheMissed(function () use ($response)
{
    $response->setBody("Hello!");
});

The response object can be fully modified during the callback. No headers will have been sent at this point, so you should not try to use this for streaming data. The callback will only be fired when Response::isNotModified() returns false.

Entity Tags

The framework has built in support for validating ETags and If-None-Match headers which will be validating during the sending of the response. If the ETag set against the response matches that on the request the response will be converted to a 304 Not Modified.

Cache Control headers

Given the nature of the Cache-Control header and it's associated ties to HTTP 1.0 headers, Wilson ships with an API to handle this gracefully. It can be accessed via the Response::getCacheControl() method call and allows for specifying options such as:


$response->getCacheControl()->makePublic()
                            ->age(400);

The framework then generates the necessary headers (such as Expires) when the response is being prepared for sending. See Wilson\Http\CacheControl for all supported options.

Cookies

The framework has very basic Cookie based on that in Symfony HttpFoundation:


$cookie = $request->getCookie("my_cookie");
echo $cookie->value, PHP_EOL;

$cookies = $request->getCookies();

$response->addCookie(new Cookie("wilson_cookie", "wilson_rocks"));

See the Wilson\Http\Cookie object for full capabilities.

Events

Preparing Requests and Responses

The framework allows you to setup headers/parameters against the request/ responses prior to the routing of the request.


$api->prepare = function (Request $request, Response $response)
{
    // ...
};

Error Handling

If an exception is thrown during the dispatch of the request, the framework exposes a slot to handle this event:


$api->error = function (Request $request, Response $response, Services $services, Exception $exception)
{
    // ...
};

Not Found Handling

If a request cannot be routed, the framework exposes a slot to handle this event:


$api->notFound = function (Request $request, Response $response, Services $services)
{
    // ...
};

Next: Services