PSR-7 is pretty close to completion. PSR-7 is a new ‘PHP standard recommendation’, put out by the PHP-FIG group, of which I’m a member of.
It describes how to create PHP representations of a HTTP Request and a HTTP response. I think the potential impact of PSR-7 can be quite large. If large PHP framework authors are on board (and at least some of them are), it means that these interfaces might in the future be used indirectly or directly by an extremely large portion of the PHP community.
PSR-7 gets a lot of things right, and is very close to nailing the abstract data model behind HTTP, better than many other implementations in many programming languages.
But it’s not perfect. I’ve been pretty vocal about a few issues I have with the approach. Most of this has fallen on deaf ears. I accept that I might be a minority in feeling these are problems, but I feel compelled to share my issues here anyway. Perhaps as a last attempt to sollicit change, or maybe just to get it off my chest.
If anything, it will allow me to say ‘I told you so’ when people start to using it and run into the edge cases that it doesn’t cover well.
PSR-7 doesn’t just represent a HTTP request and HTTP response in PHP, it tells you how to build your HTTP application.
Immutability
More recently in the process the decision has been made to make the objects immutable. This means that after the objects have been created, they are set in stone and cannot be changed.
In practice, this means instead of this:
<?php
$response->setHeader('X-Powered-By', 'Captain Planet');
?>
We need to do:
<?php
$response = $response->withHeader('X-Powered-By', 'Captain Planet');
?>
The difference is small in this isolated example, but the impact is massive.
One obvious issue is that for every change that you want to make to request or response objects, an entirely new instance needs to be created.
This bit of code creates a total of 4 copies of the request.
<?php
$request = $request
->withMethod('POST')
->withUrl(new Url('http://example.org/')
->withHeader('Content-Type', 'text/plain');
?>
The real impact in ‘time spent’ was proven to be quite low, so this part of the argument doesn’t really bother me. Cloning objects is apparently pretty cheap in PHP.
What bothers me a bit more is that this is a pretty major departure of how we are used to using these objects. Most PHP frameworks will have some type of representation of the request and response object, and many APIs that use those objects. By forcing immutability, most of this APIs will have to change.
This decision has been made for sake of robustness. This apparently would “remove a whole class of bugs”. Well, I believe the confusion that comes with an unusual API will definitely open the doors to a whole new class of bugs as well ;).
Silex
To give you an example of an API that is forced to change, here’s an example from Silex. Silex has a set of events that allows a user to alter request and response objects:
<?php
$app->before(function (Request $request, Application $app) {
// Change request and optionally return a response early.
});
$app->after(function (Request $request, Response $response) {
// Change the response before its sent
});
?>
The before
method is used to alter the request object, and can potentially be used to send an early response and ‘bypass’ the normal request flow.
Likewise, after
is used to modify a response before it’s being sent to a client.
Altering to make this work with PSR-7 implies that these relatively simple methods need a new way to send back their altered responses.
One way to do that, is with references:
<?php
$app->before(function (Request &$request, Application $app) {
// ...
});
$app->after(function (Request &$request, Response &$response) {
// ...
});
?>
People tend to not really love references though, because it’s not really obvious from the code that calls the function that it’s going to alter the value of the argument.
I imagine that the functional folks (from which this immutable API stems from) would feel better if these functions didn’t alter their arguments, but instead emit function results.
A better way might be to define an all-new object that is mutable but contains references to both the request and response:
<?php
interface RequestContext {
function getRequest();
function setRequest();
function getResponse();
function setResponse();
}
?>
Although if we want to stick to the PSR-7 philosophy, this RequestContext object should itself also probably be immutable and return modified clones of itself.
To sum up my first issue: Nearly anyone who does something useful with Request and Response objects today and wants to switch to PSR-7, will likely have to rethink large parts of their application and APIs. These objects and the style chosen for them are infectious in nature and very opiniated in their design.
The issue with streams
This is where things get a little bit out of hand. One nice feature of both HTTP in a PHP server and HTTP clients, is that we can work with very large objects, without having to use a large amount of memory.
If, for instance, I would like to send back a file from my filesystem via PHP to a client, I can do so:
<?php
stream_copy_to_stream(
fopen('massive_movie.mp4', 'r'),
fopen('php://output')
);
?>
Likewise, it’s possible to
- read
php://input
for very large requests - use streams for when making a very large http request using a client.
- use streams to read large http responses (in guzzle for example).
An important aspect of these streams is that they are often only readable once. What this means in reality, is that this breaks one of the core concepts of immutability.
<?php
// Given that $immutable is some immutable object
$immutable = '...';
any_function($immutable);
// Imutable _must_ still be in the exact same state now.
?>
Well, PSR-7 requests and responses are not like that:
Example
<?php
// Returns a request body
$request->getBody()->getContents();
// Returns an empty string.
$request->getBody()->getContents();
?>
In short this means that these objects are actually not immutable. This poses bigger problems, because when somebody changes a header:
<?php
$newRequest = $oldRequest->withHeader('X-Powered-By', 'Don Cheadle');
?>
After this operation, changes to $oldRequest
can actually influence changes to $newRequest
. Not really according to the spec, but at least to the most prominent current implementation of PSR-7, phly/http:
<?php
$newRequest = $oldRequest->withHeader('X-Powered-By', 'Don Cheadle');
// Request body
$newRequest->getBody()->getContents();
// Empty string!
$oldRequest->getBody()->getContents();
?>
This is fixable though, phly/http could in theory ensure that the request body is also ‘cloned’ when withHeader
is called, but this is also an issue.
We already established that every mutation requires a new instance. Requiring a new instance of the stream object for every mutations means that either:
- It needs to always be a string for efficient copying, thus increasing memory requirements.
- Keep it a PHP stream under the hood, and do an expensive copy operation for every ‘mutation’. This increases CPU requirements greatly.
Technically, neither of these are very good solutions.
Big responses
Lastly, it’s not clear to me in this new architecture how we can easily generate big responses on the fly.
This function, taken from StackPHP (sorry guys) is how PSR-7 wants you to think about HTTP applications:
req → λ → rep
The idea is that a function (your application) takes a request (req) and emits a response (rep).
After the response is created, it could be sent off to a client.
In PHP though, sometimes we just want to send a lot of bytes to php://output
or even just call echo()
or readfile()
. Sometimes we want to generate large streams of XML or JSON and not keep it in memory, or we may want to do an EventSource implementation.
I feel that the PSR-7 architecture forces us to buffer entire responses before we can send them off. In my ideal world, a $request
object literally wraps php://output
and writing to its body instantly emits the data, but unless I’m mistaken this is in direct conflict with the philosophy of PSR-7.
Am I going to use PSR-7?
I’m very much on the fence. sabre/dav is an application that heavily makes use of all things HTTP, and is the type of application that would run into these edge cases.
Request and Responses are everywhere and are also modified often. To adopt PSR-7 means that the plugin api needs to be almost completely rewritten, which also means that a lot of people will be affected by it.
Furthermore, I can not easily emit large responses (yes we actually need to optmize for cases where we’re sending 450MB worth of XML data for a single response), and PSR-7 does just not handle this well, at least not well without ‘working around’ how the API thinks I should build the application. Buffered responses would clearly be a big issue here.
On the other hand this would make my application more like others and this would potentially open the door for people adding generic PSR-7 middleware, which is really cool. Currently we have both a ‘PHP League’ and a Symfony adapter for requests and responses and it would be nice to not need that at one point.
The impact is severe though. Perhaps I’m too specialized for PSR-7 to make sense? Regardless, if we don’t end up with PSR-7, we’ll definitely take inspiration from it. It does contain more than a few other great ideas.
We can always just create adapters for PSR-7 objects so we can both be compatible with PSR-7 and not deal with its drawbacks.
via https://evertpot.com/psr-7-issues/
查看原文下面还有许多评论