JSON Responses in Guzzle 6

Web Development by Jacob Emerick

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)

  1. berdyshev

    Hey, thanks for the article, it helped me a lot.
    I don't understand one point: why you implement JsonSerializable interface, when you are decoding data and this interface is for encoding data. Does it helps you somehow? Why just not override getContents() method or implement custom one, i.e. getJsonData()?
    Or I'm missing something?

    1. Jacob Emerick

      Hi berdyshev,

      That's a great question :) Honestly, I was struggling coming up with a good method name and decided to go for something that has a standard interface. You're right, JsonSerializable is for serializing while this is deserializing, and a bit more creativity could have avoided any confusion.

    2. Mark Friedrich

      Thanks, great article! - I came up with the same question as "berdyshev".
      So I get rid of the JsonSerializable interface and overwrite the getContents() function from StreamDecoratorTrait:
      <pre>
      class JsonStream implements StreamInterface {

      // we need to "overwrite" the default Trait getContents() method
      // -> therefore we make it accessible as traitGetContents() and call it from
      // the new getContents() method
      use StreamDecoratorTrait {
      StreamDecoratorTrait::getContents as traitGetContents;
      }

      /**
      * @return mixed|string|null
      */
      public function getContents(){
      $contents = (string) $this->traitGetContents();

      if($contents === ''){
      return null;
      }
      $decodedContents = \\GuzzleHttp\\json_decode($contents);

      if(json_last_error() !== JSON_ERROR_NONE){
      throw new \\RuntimeException('Error trying to decode response: ' . json_last_error_msg());
      }

      return $decodedContents;
      }
      }
      </pre>

    3. Jacob Emerick

      Thanks Mark, appreciate the detailed example sir!

  2. Jonathan

    Thanks! I was trying to make such a thing in my custom API client.

  3. Ethan

    Thanks, Jacob. I'm encountering this very issue while working on a client for the Heroku Platform API. They send a variety of status codes and even use headers as a separate channel for things like pagination, so it is very handy to give the user a Psr-7 response object through which they can interact with those properties. And yet it overly complicates the consumption of the JSON payload, adding a couple lines of rote actions to each call.

    My solution is to decode the JSON and return the resulting stdClass object, but also to slip the original response object inside of it under a property named _httpResponse. This way the most basic interactions will be as easy as possible, and the response object interactions will only be one jump away:

    <pre>
    # Make a call to get Thing with ID 123.
    $thing = $myClientInstance->get('/thing/123');

    # It's easy to use the results you usually care about.
    myApp::doSomething($thing->importantProperty);

    # It's pretty easy to use any feature of the original response.
    if ($thing->_httpResponse->getStatusCode() != 200) {
    // Oh no!
    }
    </pre>

    1. Jacob P Emerick

      Nice! I actually just ran into this issue again working on some framework middleware and am _really_ trying to avoid this hack. The PSR-7 does make a lot of things more consistent, it's just these little things where forcing a standard, even when it makes sense to, gives us little headaches along the way.