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

Published by

Code Review

CodeReviewVideos is a video training site helping software developers learn Symfony faster and easier.

2 thoughts on “Symfony with Redis”

  1. Thank you for the blog post.
    I have only one question.

    $cacheKey = md5(‘fetch_widget_prices’);

    Why you use md5 here? You have a static string and it will always produce the same hash. Also, it doesn’t make sense to not have readable cache key in Redis. It’s not a memcached. I would say that on of the Redis powers is different data structures and UI interface. So, if you have readable cache name – it’s easier to maintain.
    In this example, you use Redis just as memcached, just for cache, then it’s maybe ok. But in my projects, I would store raw data into memcached and some meaningful data (counters, tmp data structures) in Redis.

    1. re: the md5, Good question.

      The example is adapted from a different project, and I had to make it generic without making it lose the point.

      I use md5 because in the original code, the keys look more like this:

      $cacheKey = md5('fetch_widgets_' . $page . '_' . $limit . '_' . $orderBy);

      $page is a simple int.

      $orderBy is a bit more complex. It is an object with a __toString() method.

      The idea being that each page of a query can be cached. Seems to work well for my needs.

      So that’s a roundabout way of saying – sometimes the keys are really long, or contain characters that might not always play nicely, so for simplicity I opt for md5.

      This makes the key names rather useless from a human point of view, but in this particular back-end I have a human readable string > md5 representation for each cache entry displayed internally. It’s a little extra work, but I only have between 5-10 entries on average, so no major headache either.

      Now, why Redis and not Memcached? That’s another good question.

      The last place I worked at, we went back and forth between adding Memcached to the existing host, versus the sysadmins preferable “let’s use Redis, instead”. The reasoning being he didn’t want to touch the live stack to add Memcached, and could ‘bolt-on’ Redis on a different physical server (I assume, maybe a VM) altogether. Less risk. Saying that, they were running PHP5.3 on prod, so they had different risks altogether, but hey 🙂

      On the final point, yes this example is simply to show Redis as a caching back end. The reason for this is that with Docker, to the best of my knowledge, I would need to re-build the PHP image to include Memcached, which I don’t always want. Keeping the two concepts separate makes this example easier, but conceptually, it doesn’t (or shouldn’t) matter whatever caching adapter you prefer.

      Lastly in this case, you could – to the best of my knowledge – use one cache pool for your cached widget prices, where you have md5’d keys. And then another cache pool entirely for your counters, or any other stuff. I can’t say I have ever pushed Redis beyond some basic caching needs honestly, but then most of what I do generally is relatively simple and small projects where Redis is totally overkill / yet another thing to maintain.

      I hope that helps explain 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.