Terminology
-
FIG Framework Interop Group
-
PSR: PHP Standards Recommendation
-
HTTP: HyperText Transport Protocol
-
CGI: Common Gateway Interface
-
SAPI: Server API
- FIG exists to codify commonalities between frameworks, and thus
promote interoperability between them. With PSR-7, the upcoming
container-interop proposal, caching, and a few others in the
background, in many ways it's aim is to subvert and remove the
need for frameworks.
- PSR comes from JSR, Java Standards Recommendation. It's also
similar to Python's PEP (Python Enhancement Proposals), though
it's at the userland, not core implementation level.
PSR-7
HTTP Message Interfaces
- PSR-7 is a set of PHP interfaces modeling value objects that
mirror HTTP messages.
POST
- Works for application/x-www-form-urlencoded and
form/multipart only…
- and only when submitted via POST.
- What about PUT and PATCH and DELETE?
- What about JSON or XML?
HTTP Request Headers
- Available as $_SERVER variables…
- but not all under the same prefix…
- and using a non-intuitive normalization.
- Most use the HTTP_ previx, but for compatibility
with CGI, "CONTENT_*" headers do not.
- All uppercase, underscore separated... when HTTP headers are
considered case insensitive, and typically use dash separation.
Request URI
- Canonical source varies based on SAPI…
- and full URI requires investigating up to 9
CGI/SAPI variables!
- SCHEME, HTTP_X_FORWARDED_PROTO, HOST, SERVER_NAME, SERVER_ADDR,
REQUEST_URI, UNENCODED URL, HTTP_X_ORIGINAL_URL, and ORIG_PATH_INFO
- This is, again, CGI's fault
File Uploads
- PHP only automatically handles them on POST requests.
- And has a few funky issues with arrays of file
uploads…
File uploads (continued)
Given $files[0] and $files[1], you would
expect:
[
'files' => [
0 => [
'name' => 'file0.txt',
'type' => 'text/plain',
/* etc. */
],
1 => [
'name' => 'file1.html',
'type' => 'text/html',
/* etc. */
],
],
];
File uploads (still)
But PHP gives you:
[
'files' => [
'name' => [
0 => 'file0.txt',
1 => 'file1.html',
],
'type' => [
0 => 'text/plain,
1 => 'text/html',
],
/* etc. */
],
];
- Which is completely unintuitive
- People don't know about this until they encounter it; the manual
doesn't make the difference clear.
Streams
- PHP abstracts both input and output as streams
- but actively makes dealing with those streams
difficult.
- Before PHP 5.6, php://input was read-ONCE,
which could lead to unexpected issues if you tried reading more
than once.
- Understanding output buffering is a black art, and requires
understanding a lot about how PHP works under the hood to get
right.
Abstractions
- The upshot is that while PHP offers easily useful and consumed
abstractions via the superglobals, there's enough difficulty and
confusion that people feel the need to provide additional
abstractions.
- Enter the frameworks!
Every framework creates their own
- Zend Framework
- Symfony
- Aura
Even client applications!
- Guzzle
- Buzz
- Requests
- Zend\Http\Client
Too many abstractions==Babel
- Too many abstractions means nobody speaks the same, and users
have to learn new abstractions anytime they pick up a new
framework or HTTP client library.
HTTP Message abstractions should be a commodity
- You cannot currently switch from ZF to Symfony or Aura easily due
to the differences in abstractions, and the learning curve required to
pick up new abstractions.
- The same is true for any HTTP client library.
- Considering the web is the lingua franca of PHP,
shouldn't these be something we all share?
Requests have a specification
POST /path HTTP/1.1
Host: example.com
Accept: text/html
Message body
- Request line is the METHOD, a request target, which is usually
the path, and the HTTP protocol used
- Then you have headers.
- Then you have the message body, which can be empty.
Responses have a specification
HTTP/1.1 200 OK
Content-Type: text/html
<b>Success!</b>
- Status line is the HTTP protocol, status code, and optionally a
reason phrase (which HTTP/2 omits)
- Then you have headers.
- Then you have the message body, which can be empty.
- The point is that the message formats are well-known and
well-specified; they should have built-in support in the language!
Benjamin Eberlei
HTTP Client interfaces
March 2012
- Essentially, Benjamin noticed that there was a lot of
commonality in how HTTP clients did their work, and felt that
could be abstracted, making it simpler to swap out clients based
on capabilities (such as ability to work with certain adapters,
use TLS vs SSL, etc.)
Chris Wilkinson
HTTP Message Interfaces
December 2012
- Chris noted that discussion around the HTTP client interfaces
had reached the conclusion that they were reliant on having robust
HTTP Message interface specifications in place first, and proposed
a set of interfaces around that.
- One other item noted was that these could be useful for
server-side applications as well.
Michael Dowling
HTTP Mesage Interfaces: Draft
January 2014
- Over a year later, Michael Dowling, of Guzzle fame, published
the first draft of HTTP Message Interfaces, acting ast the Editor
for the PSR.
- Due to his background with Guzzle, these were primarily
targeting client-side applications.
- Headers were treated as a part of a message, not as a separate
object; the argument was that the spec indicates headers define
the value of a message.
- Message bodies were treated as streams; this was
revolutionary, as it ensures that usage between implementations
must be the same, and using streams is no longer an optional
optimization.
Michael Dowling
HTTP Mesage Interfaces: Redacted
August 2014
- The tyranny of the masses got to Michael, and he felt the
constant argumentation was preventing forward momentum.
- Regardless, he planned to continue modeling Guzzle on the latest
version of the draft.
Matthew Weier O'Phinney
HTTP Mesage Interfaces: Draft
September 2014
- My team had just finished a prototype of Apigility in Node.js
- I had ported Sencha Connect to PHP, and, in doing so, chosen to
target PSR-7 — which had required creating a PSR-7 implementation.
- I was interested in the server-side ramifications.
Additions to interfaces
-
ServerRequestInterface to handle:
- PHP superglobal-type request parameters
- common concerns such as routing parameters
- message body parameter abstraction
-
UriInterface to model the URI.
-
UploadedFileInterface to model file
uploads.
- Immutable value objects
- Thank Larry Garfield for his assistance with the UriInterface
- Thank Bernard Shussek for his contribution of the
UploadedFileInterface
- We'd been modeling them as value objects, but decided to take
this to the logic conclusion of immutability.
All Messages
$headerValues = $message->getHeader('Cookie'); // array!
$headerLine = $message->getHeaderLine('Accept'); // string!
$headers = $message->getHeaders(); // string => array pairs
$body = $message->getBody(); // StreamInterface
$message = $message->withHeader('Foo', 'Bar');
$message = $message->withBody($stream);
RequestInterface — Clients
$body = new Stream();
$stream->write('{"foo":"bar"}');
$request = (new Request())
->withMethod('GET')
->withUri(new Uri('https://api.example.com/'))
->withHeader('Accept', 'application/json')
->withBody($stream);
This is how you'd create a request — which will be primarily useful
for clients.
UriInterface
$scheme = $uri->getScheme();
$userInfo = $uri->getUserInfo();
$host = $uri->getHost();
$port = $uri->getPort();
$path = $uri->getPath();
$query = $uri->getQuery();
$fragment = $uri->getFragment();
$uri = $uri->withHost('example.com');
ResponseInterface — Clients
$status = $response->getStatusCode();
$reason = $response->getReasonPhrase();
$contentType = $response->getHeader('Content-Type');
$data = json_decode((string) $response->getBody());
And on the flip side, if I want to consume a response, I have a
similar OOP interface -- in fact, it mirrors that of requests for
items like headers and the body.
ServerRequestInterface
$request = ServerRequestFactory::fromGlobals();
$method = $request->getMethod();
$path = $request->getUri()->getPath();
$accept = $request->getHeaderLine('Accept');
$data = json_decode((string) $request->getBody());
// $request = $request->withParsedBody($data);
// $params = $request->getParsedBody();
$query = $request->getQueryParams();
$cookies = $request->getCookieParams();
$files = $request->getUploadedFiles();
Consuming a request is similar to creating it - you have access to
every request member. For server-side requests, you also get access
to values usually in superglobals, such as $_GET, $_COOKIE, etc.
Note that URI abstraction is also included - you don't need to
parse the URI to get at the various segments.
ServerRequestInterface — Attributes
Problem: how do we represent parameters matched by routing?
foreach ($matchedParams as $key => $value) {
$request = $request->withAttribute($key, $value);
}
// Later:
$id = $request->getAttribute('id', 1);
- This was one place where we invented something, because every
framework routes, but they vary in how those matches are
propagated through the application.
Uploaded Files
$size = $file->getSize();
$error = $file->getError(); // PHP file upload error constant
$name = $file->getClientFilename();
$type = $file->getClientMediaType();
$stream = $file->getStream(); // StreamInterface!
$file->moveTo($targetPath);
- moveTo() abstracts the operation of moving the file, which
allows the abstraction to be used in non-SAPI environments such as
ReactPHP!
- Retrieving as a stream allows streaming a file upload somewhere,
such as S3, in a performant way that reduces memory usage.
ResponseInterface
$body = new Stream();
$stream->write(json_encode(['foo' => 'bar']));
$response = (new Response())
->withStatus(200, 'OK!')
->withHeader('Accept', 'application/json')
->withBody($stream);
Creating a response allows you to set all aspects of the response,
from status to headers to the body.
PSR-7 in a nutshell
Uniform access to HTTP messages
Who cares? Why not just use one of the existing request/response
abstractions from an existing framework? Perfectly valid. It's what
having a uniform interface enables that's interesting.
Middle what?
- Middleware transforms a request into a response.
- Typically used in server-side applications, but has been used
successfully in client-side as well; recent Guzzle versions make
extensive use of it.
- Popularized by WSGI, Rack, and Node.
Common Styles
Lambda
function (ServerRequestInterface $request) : Response
- This is a literal interpretation of the pattern.
- Laravel 5 and Lumen use this pattern; StackPHP kind of does.
Common Styles
Injected Response
function (
ServerRequestInterface $request,
ResponseInterface $response
) : ResponseInterface
- Passing the response means that the consumer does not need to
depend on a concrete response, but can depend on the abstraction.
- This is what ZF2 uses via the DispatchableInterface.
Q: How do you accomplish complex behavior?
A: By composing middleware.
class ClacksOverhead
{
public function __construct(callable $app) {
$this->app = $app;
}
public function __invoke($request, $response) {
$response = $this->app->__invoke($request, $response);
return $response->withHeader(
'X-Clacks-Overhead',
'GNU Terry Pratchett'
);
}
}
- Essentially, you can create complex behaviors by nesting
middleware. This requires that the middleware have a standard way
of injecting middleware, however…
Common Styles
Injected "Next" Middleware
function (
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
) : ResponseInterface
Invoking the "Next" middleware
// route the request
// and now inject it:
foreach ($matches as $key => $value) {
$request = $request->withAttribute($key, $value);
}
return $next(
$request,
$response->withHeader(
'X-Clacks-Overhead',
'GNU Terry Pratchett'
)
);
- Instead of convention-based injection of middleware to nest,
it's now explicit; you call $next() if there's more
processing to do.
-
$next is given the request and response to pass to
the next middleware. This is where immutability becomes very
interesting!
- Used by Relay, Slim v3, Stratigility/Expressive.
Why is middleware important?
An end to framework silos
We've all seen it: Symfony-specific bundles, ZF2-specific modules,
Laravel-specific packages. These mean that people are solving the
same problems over and over again, but wrapping them up as
framework-specific solutions. We need to stop that.
Make frameworks consume middleware
class ContactController
{
private $contact;
public function __construct(Contact $contact)
{
$this->contact = $contact;
}
public function dispatch(Request $req, Response $res)
{
return call_user_func($this->contact, $req, $res);
}
}
Hypothetical at this point, but demonstrates the idea: the framework
now consumes generic middleware. Theoretically, the framework could
simply route to generic middleware!
Wrap frameworks in middleware
$app->pipe(function ($req, $res, $next) {
$framework = bootstrap_framework();
$response = $framework->run(
convertRequestToFramework($req),
convertResponseToFramework($res),
);
return convertFrameworkResponse($response);
});
- Once you get here, you can mix and match code from frameworks,
letting each do what it does best, or…
- migrate from frameworks to middleware
The future of HTTP in PHP is collaborative
The future of HTTP in PHP is now!
Thank You
Matthew Weier O'Phinney