Bonus - Refactoring Part 2


By the end of the previous video we had discussed how making calls to new up a client - whether it be Guzzle, Buzz, or some other, is to be considered bad practice and should be avoided.

This isn't a Symfony problem, this is a general programming bad practice.

We have also seen that in order to create a HTTP client that we ultimately must call new somewhere, and that we had effectively hit a catch-22 situation.

Unfortunately, solving this is not that straightforward - at least, not to the very best of my knowledge. Feel free to correct me if I am wrong here.

In order to get a working Guzzle (or Buzz, or whatever) client, the Symfony way would be to create a new service definition which takes in the fully qualified class name - in our case this might be to the Guzzle client - and then defines a Symfony service name for that new client, which we can then inject into the service that needs the client...

And at that point, especially as a beginner, you might be scratching your head thinking - WTF?

I agree. This is not simple.

Fortunately, there is a simpler way.

Using A Third Party Bundle

We can make use of third party bundles which have already done this hard work for us. In this instance I am going to use Eightpoints Guzzle Bundle, but there is also CSA Guzzle Bundle, or MISD Guzzle Bundle, and likely others as well.

I chose Eightpoint's Guzzle Bundle simply because it came up first for a search of 'guzzle symfony' on packagist. There is no other reason. Prior to this I had been using MISD Guzzle Bundle in previous projects, but that bundle - as far as I am currently aware - only supports up to Guzzle 3.

Bundle Tradeoffs

There are downsides to using third party bundles - after all, we have just stated that doing all this hard work is hard, but after doing it you would not only control the implementation, but hopefully also have some, if not a complete understanding of how it all fits together.

When using a bundle, unless you do a deep dive yourself, effectively you are trusting the third party and relying on their given abstraction. As usual with development, this is a trade off which you as a programmer need to make based on the information you have for your current project.

Personally, I like to use third party bundles whenever possible - why reinvent the wheel? But I would say that 1 in 4 developers I have worked with over the years have taken the polar opposite viewpoint, so to each their own.

Symfony Bundle Installation

All Symfony bundles follow a very similar 'installation' process.

First you use packagist to find your bundle, then run the appropriate composer require statement to add the third party code to your project.

In our case this would be :

composer require eightpoints/guzzle-bundle

Once that code is downloaded (it will end up in your /vendors directory) then you need to go ahead and tell Symfony about it:

<?php

// /app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

            new EightPoints\Bundle\GuzzleBundle\GuzzleBundle(),

            new AppBundle\AppBundle(),
        ];

And then each bundle will come with its own set of config, which we can add in to config.yml, or override for development by using config_dev.yml. In this case we will add in just enough config to create a single client, but as we have already got our own implementation as a work in progress, I'm going to avoid setting up a base_url:

# /app/config/config.yml

guzzle:
    clients:
        8p_guzzle_client:
            base_url: ""

With this config in place we now have a Guzzle service we can inject into our GuzzleHttpClient implementation, removing the need to new up a Guzzle client inside the constructor.

Firstly, we need to update our guzzle_http_client service definition:

# /app/config/services.yml

services:
    guzzle_http_client:
        class: AppBundle\Service\GuzzleHttpClient
        arguments:
            - "@guzzle.client.8p_guzzle_client"

    # * snip *

What this is doing is telling Symfony to pass whatever is configured as the guzzle.client.8p_guzzle_client to be our first argument inside the constructor of our AppBundle\Service\GuzzleHttpClient class.

We therefore need to update the constructor appropriately:

<?php

// /src/AppBundle/Service/GuzzleHttpClient.php

namespace AppBundle\Service;

class GuzzleHttpClient implements HttpClientInterface
{
    private $client;

    public function __construct(\GuzzleHttp\Client $client)
    {
        $this->client = $client;
    }

    public function get($url)
    {
        $response = $this->client->get($url);

        return json_decode($response->getBody()->getContents(), true);
    }

And at this stage, we should - in theory at least - be good to go.

We've swapped out the new instance of Guzzle's client for the injected variety, so our implementation shouldn't need any changes... in theory.

However, in practice I did need to make a change. Confusing:

    public function get($url)
    {
        $response = $this->client->get($url);

        // had to remove the ->getContents() call
        return json_decode($response->getBody(), true);
    }

Now honestly, I don't understand why this change was required. As I say, it should have been a like-for-like swap, surely? But apparently not. And therein lies a problem with using third party bundles.

Still, with that small change we now do have a working GuzzleHttpClient once again. And now we can more easily test our client by injecting a mocked Guzzle client and so on, and so forth.

Also, as part of the Eightpoints Guzzle Bundle we also get a web debug toolbar helper, along with various events that are dispatched and can be listened too as part of your request / response lifecycle.

Of course, our Buzz implementation still goes with the new approach, so we would either need to replace this too with a third party bundle, or create our own.

If you have a differing opinion, or a different approach then by all means do leave a comment below. I'm always keen to better my understanding, and improve my own practices.

Apologies for the audio glitches in this video. I believe I have finally tracked down the root cause of this, so hopefully this won't reoccur. Fingers crossed.

Code For This Course

Get the code for this course.

Episodes