This protocol is almost a straight port of Ruby's Rack Specifications.
The basic principle of Inject\Stack is a stack of layers — so called middleware — which perform specific actions and then passes the request on to the next layer, ultimately reaching an endpoint which performs the bulk of the operations required to create a response (think of an endpoint as an action in a Controller from the MVC [1] pattern). The endpoint might pass this on to another stack which leads to another endpoint, or something completely different... you get the idea.
When an endpoint is done executing, its response will be returned through all the middleware, enabling them to finish processing of the request and finally let the browser see the result.
In more general terms, the application interface only requires one thing:
an object implementing the method __invoke($env)
which incidentally also
includes PHP Closures. The $env
parameter and the return value of the
application are required have a specific format, see The Environment Variable
and The Return Value.
To make the creation of the stack easier, the class \Inject\Stack\Builder
and the interface \Inject\Stack\MiddlewareInterface
has been introduced.
\Inject\Stack\Builder
constructs a stack from supplied concrete instances
of the \Inject\Stack\MiddlewareInterface
and a final endpoint implementing
the method __invoke($env)
.
An endpoint is a PHP object implementing __invoke($env)
method (which
also includes PHP closures taking a single parameter). Usually the main
endpoint of your application will be a Router which in turn will call the
controller, or a controller specific \Inject\Stack\Builder
or something else.
Running of a simple endpoint:
<?php class MyEndpoint { public function __invoke($env) { return array(200, array('Content-Type' => 'text/plain'), 'Hello World!'); } } // Or even: $hello_endpoint = function($env) { return array(200, array('Content-Type' => 'text/plain'), 'Hello World!'); }; // Run your application: $adapter = new \Inject\Stack\Adapter\Generic(); $adapter->run($hello_endpoint);
For a more complicated endpoint, see the \Inject\Stack\CascadeEndpoint
.
This endpoint attempts several callbacks until one does not return a
response with the status code (first array index in the response) set to
404
. So the associated callbacks will return a response along the lines
of array(404, array(), '')
if they do not process the request.
If you have used Ruby on Rails or Ruby's Rack webserver interface you probably already know what it is as it is almost a port.
A middleware is an object implementing
Inject\Stack\MiddlewareInterface
which specifies a basic
interface for middleware. This interface enforces two public methods:
setNext(callback $next)
(sets the callback pointing to the next
layer/endpoint) and __invoke($env)
which performs the middleware
logic and then (if the middleware logic allows) forwards the request to
the next middleware or endpoint.
The main reason for the usage of an interface is that it is not feasible to inject the next middleware using the constructor of a middleware, mainly because it will not be as fast or flexible in PHP as it is in Ruby.
Here is an example middleware which checks for the $_GET
parameter "go" and
returns a 404 if it cannot find it:
<?php namespace MyNamespace; use \Inject\Stack\MiddlewareInterface; /** * Will return a 404 if the GET key "go" does not exist. */ class BlockIfNotGo implements MiddlewareInterface { protected $next; public function setNext($callback) { $this->next = $callback; } public function __invoke($env) { if( ! isset($env['inject.get']['go'])) { return array(404, array('Content-Type' => 'text/plain'), 'Page not found'); } $callback = $this->next; return $callback($env); } }
For a simple middleware which does something more useful, look at
\Inject\Stack\Middleware\RunTimer
which times the execution of all the
nested middleware and endpoint(s), code called by those and finally adds
this in an X-Runtime
response header.
The environment variable, usually referred to as $env
, is a hash
(PHP array with string keys) which is passed through all the layers
of the middleware stack. This hash contains a list of CGI like-headers (as
$_SERVER
usually looks like).
The base for this $env
variable is usually the global $_SERVER
variable as it already contains many of the headers which are used
by PHP applications and also the information needed to run said
application and its components.
$env
is not a static hash, all components of the system are allowed
to modify the environment to, for example, add a global object, filter a
specific header or change something like the REQUEST_METHOD
. This
can be very useful when for example performing internal HMVC [2] requests,
as you can copy the $env
variable and change a few keys before
passing it on to the internal controller.
The environment variable must however conform to a few basic rules:
The Environment variable must always include these keys:
REQUEST_METHOD
:- The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required. Uppercase.
SCRIPT_NAME
:The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server (in the case of URL rewriting).
If it is not empty it must start with a
/
, it must never contain/
by itself.PATH_INFO
:The remainder of the request URL's "path", designating the virtual "location" of the request‘s target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
If it is not empty it must start with a
/
, ifSCRPT_NAME
is empty, it must be/
.BASE_URI
:The URI prefix to be used when referring to static assets which are not processed by the application logic.
This is usually the URI without the
index.php
file name, and will usually be taken care of by the concrete class implementing\Inject\Stack\AdapterInterface
.QUERY_STRING
:- The portion of the request URL that follows the ?, if any. May be empty, but is always required!
SERVER_NAME
,SERVER_PORT
:- When combined with SCRIPT_NAME and PATH_INFO, these variables can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT can never be empty strings, and so are always required.
REMOTE_ADDR
:- The IP address of the remote connection which the server received.
HTTP_
Variables:- Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose names begin with HTTP_). The presence or absence of these variables should correspond with the presence or absence of the appropriate HTTP header in the request.
Inject\Stack's AdapterInterface
implementations will include these keys:
inject.version
:- The current version of Inject\Stack.
inject.url_scheme
:https
orhttp
, depending on the request URL.inject.adapter
:- The class name of the concrete class implementing
\Inject\Stack\AdapterInterface
which is used to run the application. inject.get
:- Contains the GET data.
inject.post
:- Contains the POST data, ie. parsed
inject.input
, provided the request'sREQUEST_METHOD
isPOST
or that theCONTENT_TYPE
isapplication/x-www-form-urlencoded
ormultipart/form-data
. inject.input
:- Stream containing the request body, will be closed by the adapter upon request completion. By default this is stream can not be rewinded!
All keys which do not contain a dot (.
) must contain string/scalar values,
if you include a dot in the name (like web.route
) there are no
restrictions on what you can use as a value.
These keys have special rules:
CONTENT_LENGTH
:- If present it must match
/^\d+$/
. HTTP_CONTENT_TYPE
:- Must not be present, rename to
CONTENT_TYPE
. HTTP_CONTENT_LENGTH
:- Must not be present, rename to
CONTENT_LENGTH
.
The return value of all middleware and endpoints is an array with three elements, containing response code, array with response headers and finally the string which is the response body:
array(response_code, response_headers, response_body)
It can also be an object implementing \ArrayAccess
, \Countable
and also \Iterator
or \IteratorAggregate
.
The value returned by $return_array[0]
must be the response code,
$return_array[1]
are the headers and $return_array[2]
contains
the response body.
Example response array:
array(200, array('Content-Type' => 'text/html; charset=utf-8', 'Last-Modified' => date(\DateTime::RFC1123), 'Cache-Control' => 'public'), '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC ...')
A plain integer which is the HTTP response code (matches /^\d+$/
and >= 100
).
Must be an array or array equivalent (\ArrayAccess
, \Countable
and also \Iterator
or \IteratorAggregate
).
All header keys are strings, and written as they are in the HTTP specification,
ie. Content-Type
instead of content-type
or content_type
. [3]
Their values cannot contain :
or \n
and must match
/^[a-zA-Z][a-zA-Z0-9_-]*$/
. The header status
is not allowed.
All header values must either be strings or objects responding to
__toString()
, and they must not contain ASCII character values
below 028
(excepting newline == 012 == \n
).
If the response code is 1xx
, 204
or 304
the Content-Type
header cannot exist. Otherwise it must be present.
If the response code is 1xx
, 204
or 304
, or if the
REQUEST_METHOD
is HEAD
, the Content-Length
header must not
exist. Otherwise it must match the length of the body (strlen($body)
)
provided that the header itself exists.
The response body is a string or an object responding to __toString()
.
It must be empty if the REQUEST_METHOD
is HEAD
.
It can also be a resource-stream which can be used with fread()
, feof()
and fclose()
. In that case adapters will read from the resource using
fread()
while feof()
!= false
, and when the stream reading has
reached EOF
the stream will be closed with fclose()
.
If the Content-Length
header does not exist and the response body is a
string or object (and not empty), it will be created and assigned with the
length of the resulting string. (Transfer-Encoding
header must be empty for this
auto-assignment)
If the Content-Length
header does not exist and the response is a
resource-stream, Chunked-Encoding will be used to transfer the data from
the stream, making it possible to deliver content which length is not yet known.
(Transfer-Encoding
header must be empty for this auto-assignment)
To validate $env
and the response of your middleware/endpoints, you may
use the \Inject\Stack\Middleware\Lint
middleware. This middleware will
validate the $env
var when it is received, and after the next
middleware/endpoint has processed the request, it will validate the response.
It is recommended to add one instance before your middleware and one after
to validate that the $env
variable is passed on correctly. If you want
to validate an endpoint, just add the lint middleware as the last middleware
before your endpoint.
If any of the assertions fail, a LintException
will be thrown, detailing
the problem
Note: Do not use this in production, however, as all the checks will slow down the request processing by a large factor.
[1] | Model-View-Controller, see Wikipedia about MVC |
[2] | Hierarchical Model-View-Controller, see Wikipedia about HMVC |
[3] | This is to prevent multiple fields for the same key (HTTP specifications
say that header keys are case-insensitive) without lots of extra code
converting them to and from the ucfirst() format specified in the
HTTP-spec. |