Symfony with Redis

In a bid to make getting up and running with CodeReviewVideos tutorials moving forwards, I have created a new repo called the Docker and Symfony3 starting point.

I will be using this as the basis for all projects going forwards as it dramatically simplifies the process of setting up each tutorial series, and should – in theory – make reliably reproducing my setups much easier for you.

I’ve had a lot of feedback lately to say it’s increasingly hard work to follow some of the older tutorials as the dependencies are out of date.

Taking this feedback on board, I will do my best to update these projects in the next few weeks-to-months. I am, however, going to wait for Symfony 4 to land before I spend any time updating stuff. No point duplicating a bunch of effort. Symfony 4 drops in November, in case you are wondering.

Video Update

This week saw two new videos added to the site. Unfortunately I was ill early in the week so didn’t get to record the usual third video.

#1 – [Part 1/2] – Symfony 3 with Redis Cache

Caching is a (seemingly) easy win.

Imagine we have some expensive operation: maybe a heavy computation, or some API request that needs to go across the slow, and unpredictable internet.

Wouldn’t it be great if we did this expensive operation only once, saved (or cached) the result, and then for every subsequent request, we sent back the locally saved result.

Yes, that sounds awesome.

Symfony has us covered here. The framework comes with a bunch of ways we can cache and store data.

Redis seems to be the one I find most larger organisations like to use, so that’s one of the reasons behind picking Redis out of the bunch.

At this point you may be thinking:

But Chris, I don’t have a Redis instance just laying around waiting for use, and I’m not rightly sure how I might go about setting one up!

Well, not to worry.

To make life as simple as possible, we are going to use Docker.

Docker… simple… the same sentence?!

Well, the jolly good news is that you don’t need to know anything about Docker to use this setup. At least, I hope you won’t. That’s the theory.

As mentioned above, I am making use of the new Docker / Symfony 3 starting point repo. I’ve tweaked this a touch for our Redis requirements.

By the end of this video you will have got a Symfony website up and running, and cached data into Redis. We will cover the setup required, both from a Docker perspective, and the config needed inside Symfony.

#2 – [Part 2/2] – Symfony 3 with Redis Cache

In the previous video we put all our caching logic into a controller action.

That’s really not going to cut it beyond your first steps. In reality you’re going to want to move this stuff out into a separate service.

To give you an example of how I might do it:

Controller calls CacheApiWrapper which calls Api if needed.

That might not be making much sense, so let’s break it down further.

Let’s imagine an app without caching.

We need to call a remote API to get the days prices of Widgets.

We hit a controller action which delegates the API call off to our Api service, which really manages the API call for us. When the API call completes (or fails), it returns some response to the calling controller action, which in turn returns the response to the end user.

If that API call always returns the same response every time it is called – maybe the prices of Widgets only change every 24 hours – then calling the remote API for every request is both pointless, and wasteful.

We could, if we wanted to, add the Caching functionality directly into the Api service.

I don’t like this approach.

Instead, I prefer to split the responsibility of Caching out into a separate ‘layer’. This layer / caching service has an identical API to the Api service. This caching service takes the Api service as a constructor argument, and wraps the calls, adding caching along the way.

If all this sounds complex, after seeing some code, hopefully it will make a bit more sense:

class WidgetController extends Controller
{
    /**
     * @Route("/widgets", name="widget_prices")
     */
    public function widgetPriceAction(
        WidgetPriceProvider $widgetPriceProvider
    )
    {
        return $this->render(
            ':widget:widget_price.html.twig',
            [
                'widget_prices' => $widgetPriceProvider->fetchWidgetPrices(),
            ]
        );
    }

This should hopefully look familiar.

The unusual part is the WidgetPriceProvider. Well, they do say the two hardest parts of computer science are cache invalidation, and naming things…

Really WidgetPriceProvider is the best name I could think of to explain what’s happening. This service provides Widget Prices.

How it provides them is where the interesting part happens:

<?php

namespace AppBundle\Provider;

use AppBundle\Connectivity\Api\WidgetPrice;
use AppBundle\Widget\WidgetPriceInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;

class WidgetPriceProvider implements WidgetPriceInterface
{
    /**
     * @var CacheItemPoolInterface
     */
    private $cache;

    /**
     * @var WidgetPrice
     */
    private $widgetPriceApi;

    public function __construct(
        AdapterInterface $cache, 
        WidgetPrice $widgetPriceApi
    )
    {
        $this->cache          = $cache;
        $this->widgetPriceApi = $widgetPriceApi;
    }

    public function fetchWidgetPrices()
    {
        $cacheKey = md5('fetch_widget_prices');

        $cachedWidgetPrices = $this->cache->getItem($cacheKey);

        if (false === $cachedWidgetPrices->isHit()) {

            $widgetPrices = $this->widgetPriceApi->fetchWidgetPrices();

            $cachedWidgetPrices->set($widgetPrices);
            $this->cache->save($cachedWidgetPrices);

        } else {

            $widgetPrices = $cachedWidgetPrices->get();

        }

        return $widgetPrices;
    }
}

As mentioned, WidgetPriceProvider is a wrapper / layer over the Api Service.

It also has the cache injected.

This way the API call is separated from the cache, and can be used directly if needed. Sometimes I don’t want to go via the cache. Maybe in the admin backend, for example.

Note that this service implements WidgetPriceInterface. This isn’t strictly necessary. This interface defines a public function of fetchWidgetPrices.

The reason for this is, as mentioned earlier, I want the caching layer (WidgetPriceProvider) to use the same method name(s) as the underlying API service.

Speaking of which:

<?php

namespace AppBundle\Connectivity\Api;

use AppBundle\Widget\WidgetPriceInterface;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;

class WidgetPrice implements WidgetPriceInterface
{
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var Client
     */
    private $client;

    /**
     * WidgetPrice constructor.
     *
     * @param LoggerInterface $logger
     * @param Client          $client
     */
    public function __construct(
        LoggerInterface $logger,
        Client $client
    )
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    public function fetchWidgetPrices()
    {
        // better to inject via constructor, but easier to show like this
        $url = "https://api.widgetprovider.com/prices.json";

        try {

            return json_decode(
                $this->client->get($url)->getBody()->getContents(),
                true
            );

        } catch (\Exception $e) {

            $this->logger->alert('It all went Pete Tong', [
                'url'       => $url,
                'exception' => [
                    'message' => $e->getMessage(),
                    'code'    => $e->getCode()
                ],
            ]);

            return [];
        }
    }
}

So really it’s about delegating responsibility down to a more appropriate layer.

Be Careful – don’t blindly copy this approach.

There is a gotcha in this approach which maybe not be immediately obvious.

If your API call fails, the bad result (and empty array in this case) will still be cached.

Adapt accordingly to your own needs. It may be better to move the try  / catch logic out from the WidgetPrice class and into the WidgetPriceProvider instead.

Or something else. It’s your call.

Anyway, that’s an excursion into a potentially more real world setup.

Hopefully you find this weeks videos useful.

As every, have a great weekend and happy coding.

Chris