Loïc Faugeron Technical Blog

Symfony / Web Services - part 3.2: Consuming, Guzzle 18/03/2015

Deprecated: This series has been re-written - see The Ultimate Developer Guide to Symfony

This is the sixth article of the series on managing Web Services in a Symfony environment. Have a look at the five first ones:

You can check the code in the following repository.

In the previous article, we've bootstrapped an application with a RequestHandler, allowing us to be decoupled from the third part library we'll choose to request remote endpoints.

In this article, we'll create a Guzzle 5 implementation.

Guzzle Request Handler

As usual, we first describe the class we want to create:

./bin/phpspec describe 'AppBundle\RequestHandler\Middleware\GuzzleRequestHandler'

Our Guzzle implementation will translate our Request into a guzzle one, and a guzzle response into our Response:

<?php
// spec/AppBundle/RequestHandler/Middleware/GuzzleRequestHandlerSpec.php

namespace spec\AppBundle\RequestHandler\Middleware;

use AppBundle\RequestHandler\Request;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Stream\StreamInterface;
use PhpSpec\ObjectBehavior;

class GuzzleRequestHandlerSpec extends ObjectBehavior
{
    const VERB = 'POST';
    const URI = '/api/v1/profiles';

    const HEADER_NAME = 'Content-Type';
    const HEADER_VALUE = 'application/json';

    const BODY = '{"username":"King Arthur"}';

    function let(ClientInterface $client)
    {
        $this->beConstructedWith($client);
    }

    function it_is_a_request_handler()
    {
        $this->shouldImplement('AppBundle\RequestHandler\RequestHandler');
    }

    function it_uses_guzzle_to_do_the_actual_request(
        ClientInterface $client,
        RequestInterface $guzzleRequest,
        ResponseInterface $guzzleResponse,
        StreamInterface $stream
    )
    {
        $request = new Request(self::VERB, self::URI);
        $request->setHeader(self::HEADER_NAME, self::HEADER_VALUE);
        $request->setBody(self::BODY);

        $client->createRequest(self::VERB, self::URI, array(
            'headers' => array(self::HEADER_NAME => self::HEADER_VALUE),
            'body' => self::BODY,
        ))->willReturn($guzzleRequest);
        $client->send($guzzleRequest)->willReturn($guzzleResponse);
        $guzzleResponse->getStatusCode()->willReturn(201);
        $guzzleResponse->getHeaders()->willReturn(array('Content-Type' => 'application/json'));
        $guzzleResponse->getBody()->willReturn($stream);
        $stream->__toString()->willReturn('{"id":42,"username":"King Arthur"}');

        $this->handle($request)->shouldHaveType('AppBundle\RequestHandler\Response');
    }
}

Time to boostrap this implementation:

./bin/phpspec run

And to actually write it:

<?php
// File: src/AppBundle/RequestHandler/Middleware/GuzzleRequestHandler.php

namespace AppBundle\RequestHandler\Middleware;

use AppBundle\RequestHandler\Request;
use AppBundle\RequestHandler\RequestHandler;
use AppBundle\RequestHandler\Response;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Stream\StreamInterface;

class GuzzleRequestHandler implements RequestHandler
{
    private $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function handle(Request $request)
    {
        $guzzleRequest = $this->client->createRequest($request->getVerb(), $request->getUri(), array(
            'headers' => $request->getHeaders(),
            'body' => $request->getBody(),
        ));
        $guzzleResponse = $this->client->send($guzzleRequest);
        $response = new Response($guzzleResponse->getStatusCode());
        $response->setHeaders($guzzleResponse->getHeaders());
        $response->setBody($guzzleResponse->getBody()->__toString());

        return $response;
    }
}

Let's check it:

./bin/phpspec run

Brilliant!

git add -A
git commit -m 'Created GuzzleRequestHandler'

Event Middleware

In the future we'd like to be able to hook in the RequestHandler's workflow, for example if the Response's body is in JSON, convert it into an array.

This kind of thing can be done by sending events, in our case when a Response is received:

<?php
// File: src/AppBundle/RequestHandler/ReceivedResponse.php

namespace AppBundle\RequestHandler\Event;

use AppBundle\RequestHandler\Response;
use Symfony\Component\EventDispatcher\Event;

class ReceivedResponse extends Event
{
    private $response;

    public function __construct(Response $response)
    {
        $this->response = $response;
    }

    public function getResponse()
    {
        return $this->response;
    }
}

Note: This is a simple Data Transfer Object (DTO), it doesn't contain any logic and never will. This means that we don't have to write any tests for it.

We could add an EventDispatcher in GuzzleRequestHandler, or we could create a middleware: a RequestHandler that dispatches events and then calls another RequestHandler (e.g. GuzzleRequestHandler):

./bin/phpspec describe 'AppBundle\RequestHandler\Middleware\EventRequestHandler'

This way if we want to throw away GuzzleRequestHandler and replace it with something else, we don't have to write again the dispatching code. Here's the specification:

<?php
// File: spec/AppBundle/RequestHandler/Middleware/EventRequestHandlerSpec.php

namespace spec\AppBundle\RequestHandler\Middleware;

use AppBundle\RequestHandler\Request;
use AppBundle\RequestHandler\RequestHandler;
use AppBundle\RequestHandler\Response;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class EventRequestHandlerSpec extends ObjectBehavior
{
    function let(EventDispatcherInterface $eventDispatcher, RequestHandler $requestHandler)
    {
        $this->beConstructedWith($eventDispatcher, $requestHandler);
    }

    function it_is_a_request_handler()
    {
        $this->shouldImplement('AppBundle\RequestHandler\RequestHandler');
    }

    function it_dispatches_events(
        EventDispatcherInterface $eventDispatcher,
        Request $request,
        RequestHandler $requestHandler,
        Response $response
    )
    {
        $requestHandler->handle($request)->willReturn($response);
        $receivedResponse = Argument::type('AppBundle\RequestHandler\Event\ReceivedResponse');
        $eventDispatcher->dispatch('request_handler.received_response', $receivedResponse)->shouldBeCalled();

        $this->handle($request)->shouldBe($response);
    }
}

Note: We could improve this middleware by dispatching an event before giving the request to the RequestHandler. We could also catch exceptions coming from the RequestHandler and dispatch an event.

Time to bootstrap the code:

./bin/phpspec run

And to make the test pass:

<?php
// File: src/AppBundle/RequestHandler/Middleware/EventRequestHandler.php

namespace AppBundle\RequestHandler\Middleware;

use AppBundle\RequestHandler\Event\ReceivedResponse;
use AppBundle\RequestHandler\Request;
use AppBundle\RequestHandler\RequestHandler;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class EventRequestHandler implements RequestHandler
{
    private $eventDispatcher;
    private $requestHandler;

    public function __construct(EventDispatcherInterface $eventDispatcher, RequestHandler $requestHandler)
    {
        $this->eventDispatcher = $eventDispatcher;
        $this->requestHandler = $requestHandler;
    }

    public function handle(Request $request)
    {
        $response = $this->requestHandler->handle($request);
        $this->eventDispatcher->dispatch('request_handler.received_response', new ReceivedResponse($response));

        return $response;
    }
}

Did we succeed?

./bin/phpspec run

Yes, we did:

git add -A
git commit -m 'Created EventRequestHandler'

Json Response Listener

When a Response contains a JSON body, we need to:

With this in mind, we can describe the listerner:

./bin/phpspec describe 'AppBundle\RequestHandler\Listener\JsonResponseListener'

Now we can write the specification:

<?php
// src: spec/AppBundle/RequestHandler/Listener/JsonResponseListenerSpec.php

namespace spec\AppBundle\RequestHandler\Listener;

use AppBundle\RequestHandler\Event\ReceivedResponse;
use AppBundle\RequestHandler\Response;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class JsonResponseListenerSpec extends ObjectBehavior
{
    function it_handles_json_response(ReceivedResponse $receivedResponse, Response $response)
    {
        $receivedResponse->getResponse()->willReturn($response);
        $response->getHeader('Content-Type')->willReturn('application/json');
        $response->getBody()->willReturn('{"data":[]}');
        $response->setBody(array('data' => array()))->shouldBeCalled();

        $this->onReceivedResponse($receivedResponse);
    }

    function it_does_not_handle_non_json_response(ReceivedResponse $receivedResponse, Response $response)
    {
        $receivedResponse->getResponse()->willReturn($response);
        $response->getHeader('Content-Type')->willReturn('text/html');
        $response->getBody()->shouldNotBeCalled();

        $this->onReceivedResponse($receivedResponse);
    }

    function it_fails_to_handle_invalid_json(ReceivedResponse $receivedResponse, Response $response)
    {
        $receivedResponse->getResponse()->willReturn($response);
        $response->getHeader('Content-Type')->willReturn('application/json');
        $response->getBody()->willReturn('{"data":[');

        $exception = 'Exception';
        $this->shouldThrow($exception)->duringOnReceivedResponse($receivedResponse);
    }
}

Time to implement the code:

<?php
// File: src/AppBundle/RequestHandler/Listener/JsonResponseListener.php

namespace AppBundle\RequestHandler\Listener;

use AppBundle\RequestHandler\Event\ReceivedResponse;
use Exception;

class JsonResponseListener
{
    public function onReceivedResponse(ReceivedResponse $receivedResponse)
    {
        $response = $receivedResponse->getResponse();
        $contentType = $response->getHeader('Content-Type');
        if (false === strpos($response->getHeader('Content-Type'), 'application/json')) {
            return;
        }
        $body = $response->getBody();
        $json = json_decode($body, true);
        if (json_last_error()) {
            throw new Exception("Invalid JSON: $body");
        }
        $response->setBody($json);
    }
}

Is it enough to make the tests pass?

./bin/phpspec run

Yes, we can commit:

git add -A
git commit -m 'Created JsonResponseListener'

Creating services

In order to be able to use this code in our Symfony application, we need to define those classes as services. Since we'll have a lot of definitions, we'll create a services directory:

mkdir app/config/services

We'll update services.yml to include our new file:

# File: app/config/services.yml
imports:
    - { resource: services/request_handler.yml }

And finally we'll create the request_handler.yml file:

touch app/config/services/request_handler.yml

The first service we'll define is Guzzle:

#file: app/config/services/request_handler.yml
services:
    guzzle.client:
        class: GuzzleHttp\Client

This allows us to define the GuzzleRequestHandler:

#file: app/config/services/request_handler.yml

    app.guzzle_request_handler:
        class: AppBundle\RequestHandler\Middleware\GuzzleRequestHandler
        arguments:
            - "@guzzle.client"

We want to wrap each of these GuzzleRequestHandler calls with events, so we define EventRequestHandler like this:

#file: app/config/services/request_handler.yml

    app.event_request_handler:
        class: AppBundle\RequestHandler\Middleware\EventRequestHandler
        arguments:
            - "@event_dispatcher"
            - "@app.guzzle_request_handler"

In the future we might add more middlewares (e.g. RetryRequestHandler, StopwatchRequestHandler, etc), so we want to avoid using a service that points directly to an implementation. We can define an alias:

#file: app/config/services/request_handler.yml

    app.request_handler:
        alias: app.event_request_handler

Finally, we want to define our listener:

#file: app/config/services/request_handler.yml

    app.request_handler.json_response_listener:
        class: AppBundle\RequestHandler\Listener\JsonResponseListener
        tags:
            - { name: kernel.event_listener, event: request_handler.received_response, method: onReceivedResponse }

And that's it!

git add -A
git commit -m 'Defined RequestHandler as a service'

Conclusion

We can now send remote request using Guzzle, without coupling ourself to the library. We even implemented an EventRequestHandler to allow extension points, it also provides us an example on how to write more RequestHandler middlewares.

In the next article we'll start using RequestHandler in a specific kind of service: Gateways.

HTTP Adapter

You might be interested in Ivory HttpAdapter, a library very similar to our RequestHandler: it sends remote request through a given client (it supports many of them). It also provides events to hook into its workflow!

Personnaly, I'd rather create my own RequestHandler, as my purpose is to decouple the application from an external library like Guzzle: using a third party library to do so seems a bit ironic. As you can see there's little effort involved, and it has the advantage of providing the strict minimum the application needs.

PSR-7

PSR-7 is a standard currently under review: it defines how frameworks should ideally implement the HTTP protocole.

Since it's not yet accepted, it is subject to change so I wouldn't recommend to follow it yet. Our RequestHandler kind of implement the HTTP protocole, but I'd rather not make it PSR-7 compliant, as it requires the implementation of many features we don't really need.

To get a balanced opinion on the matter, I'd recommend you to read:

Reference: see the phpspec reference article