PSR-7 and Middleware – The Future of PHP



PSR-7 and Middleware – The Future of PHP

4 4


2015-10-20-PSR-7-and-Middleware


On Github weierophinney / 2015-10-20-PSR-7-and-Middleware

PSR-7 and Middleware

The Future of PHP

Matthew Weier O'Phinney / @mwop

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.

Why?

Because PHP Doesn't

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!

PSR-7: A history

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.

Relationships

Acceptance: 18 May 2015

Examples

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.

Middleware

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!

Resources

Thank You

Matthew Weier O'Phinney

http://joind.in/15537

https://mwop.net/https://apigility.orghttp://framework.zend.com@mwop