JSON Responses in Guzzle 6
Guzzle is a wildly popular PHP client that makes it simple to perform HTTP requests. The latest version, Guzzle v6, implements PSR-7 standards when handling requests and responses. By enforcing this standard HTTP messaging protocol it is now easier to have inter operable exchanges between packages, as well as build interactions that have limited dependency on Guzzle itself. One small side effect of this was the removal of the "::json()" method from the response, as the PSR-7 standard does not define this.
Now, the act of retrieving a response from a service should be separate from interpreting the contents of the response. A basic client should reach out to to a service with a request, fetch data, and return it as a string (usually). As the maintainers of Guzzle pointed out in issue #1238, interpreting the contents extends beyond the interface of PSR-7. That doesn't make the missing feature less useful, though. When I recently built an API client that leans on Guzzle, for a service that will only ever returns JSON, the thought of making users of my client call "json_encode()" after ever response was unappealing. So I looked into the adding some middleware to the call stack to do this.
My first thought was not middleware. If I was already wrapping up the certain endpoints and functionalities for this client, couldn't I just wrap the response and spit out decoded JSON objects (or arrays)? The problem with this is that the PSR-7 response objects are just way too useful. They include methods to get the status code, and human-readable statuses, and headers… So much more than just the actual body. And depending on the usage of the client I was building some of this extra stuff could actually be quite useful.
So middleware it was. If I could find a way to sneak into the stack, modify the Response object, and have a useful method for retrieving JSON, then my client would return standardized, interpreted data. As I dived into Guzzle's internals I discovered an even better option: I could modify the Stream. The Response object holds onto headers and everything, but the actual body is contained in a separate implementation of StreamInterface that had a "::__toString()" method. I could add another method, say, "::jsonSerialize()" from the JsonSerializable interface in core PHP, that would decode this stream.
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use JsonSerializable;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class JsonStream implements StreamInterface, JsonSerializable
{
use StreamDecoratorTrait;
public function jsonSerialize()
{
$contents = (string) $this->getContents();
if ($contents === '') {
return null;
}
$decodedContents = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(
'Error trying to decode response: ' .
json_last_error_msg()
);
}
return $decodedContents;
}
}
One note to make about the empty string check - in PHP 7, if you pass an empty string into "json_decode()", you will get a syntax error. Previous versions of PHP would just return null with no problem. I discovered this the hard way when some of my builds started failing… Luckily, I had a unit test that tripped this condition so I was able to debug and add the check here for safety.
To implement this middleware I had to push to the stack. At first I used the generic way that is described in the docs, creating a custom handler, but stuff began to break unexpectedly. Turns out that Guzzle injects its own middleware to the default handler, so if you inject a custom handler without this stuff you're going to have a bad time. I had to push on to the default handler, not replace it, in order to keep things operating normally.
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
$stack = HandlerStack::create();
$stack->push(Middleware::mapResponse(function (Response $response) {
$jsonStream = new JsonStream($response->getBody());
return $response->withBody($jsonStream);
}));
$guzzle = new Guzzle([
'base_uri' => 'URI HERE',
'handler' => $stack,
]);
I feel mostly good about these changes. Well, I feel a little weird about how the Response body switches between Stream and casted string. It makes sense that "::__toString()" flattens a Stream, because that's what it does, but now I have a "::jsonSerialize()" that also flattens it. If there was a standard "::__toJSON()" that PHP supported I'd feel better. With how it works now, by having the stream implement "JsonSerializable", at least I'm enforcing the always-JSON property of this client. (Which, by the way, you can view at jacobemerick/php-shutterstock-api.)
Comments (7)