[Part 1/2] - Symfony 3 with Redis Cache


In this video we will cover how to use Redis as the storage for your cache in a Symfony 3.3+ application.

Redis becomes an attractive option on larger infrastructures as it allows you to separate your server(s) that handle web requests from the servers that cache your data.

This is to say that you can install Redis on one or more servers that are solely dedicated to caching. On smaller infrastructures you can often get away with having your cache on the same machine / host / server as your web server.

Why Redis is great is that it stores your cache in your servers RAM. This makes accessing the cached data much faster than if a looking up the same data in a database, or in flat files on disk.

As mentioned, Redis can be installed on a separate server(s), and in that situation then additional latency would be involved because of the network communication between your web server box and the redis host / cluster.

Other options exist to solve this problem - Memcached for example - so if you already have Memcached available on your system then consider using that instead. This process is largely the same whatever Caching adapter you choose.

Setup

In order to use Redis as a Cache in your Symfony application then you need a running instance of Redis :)

To represent this as easily as possible we are going to use Docker.

However you are making a Redis instance available (Docker, manual installation, Ansible, Puppet, etc) the process once the server is up and running will be the same.

If you are unfamiliar with Docker then please watch this course.

We're going to start by cloning an existing Dockerised stack featuring Symfony 3, nginx, and MySQL:

git clone https://github.com/codereviewvideos/docker-symfony3.git redis-symfony-example
cd redis-symfony-example

For more info on this stack, please watch this video.

We now need to install all the vendor / third party dependencies.

We do this (as I'm sure you know) with composer install.

However, here's the kicker:

If we run a composer install from our laptop / desktop directly, then Symfony will moan about not having all the expected environment variables set.

This isn't a huge problem. We can safely ignore the resulting exception.

Or, we can run composer install via the PHP docker container.

Let's do that:

docker-compose exec php composer install

The outcome is the same as running the composer install command locally, only this time the environment variables are set, so there are no runtime exceptions.

Make sure you can access the stack by running:

make dev

If this command doesn't work for you, copy / paste the command from the Makefile, eg:

docker-compose down && \
    docker-compose up -d --remove-orphans

Once this command completes (images downloaded, etc) then browse to:

127.0.0.1:81

And you should see the Symfony "Welcome" page.

Adding Redis

We're going to add Redis using Docker.

To do this we will need to add a Redis entry to our docker-compose.yml file.

Here's what we are going to add:

version: '3'

services:

    # ... other stuff removed for brevity

    redis:
        image: redis:4.0.2
        hostname: redis
        volumes:
          - "./volumes/redis:/data"

At the time of recording Redis 4.0.2 is the latest available tag. Check docker hub for a newer version if required.

The hostname key makes sure that from other machines in our stack (e.g. the php machine) we can ping redis by name. We will need this in our config.

You do not need to use a volume. I am doing so here to show one way - using a bind mount - that this could be done. I'm using a bind mount here as it is simpler on a Mac than a named volume, in my opinion. This is less of a concern on Linux. If you do not use a Volume then any data stored by Redis will be removed if the container is removed.

With this extra config in place we can reinitialise our infrastructure and start hitting Redis:

make dev

As above, if this command doesn't work for you, run the command by hand:

docker-compose down && \
    docker-compose up -d --remove-orphans

Configuring Redis

In order to talk to Redis from Symfony we will need to add in some new configuration.

Firstly we will update the parameters.yml.dist and parameters.yml files with two new entries:

# /app/config/parameters.yml.dist
#              AND
# /app/config/parameters.yml

parameters:
    # ...
    redis.host: '%env(REDIS_HOST)%'
    redis.port: '%env(REDIS_PORT)%'

We are using Environment Variables heavily in this setup. Please note, this is purely for development purposes. In production, consider using a more robust / secure means of managing your configuration. Also, feel free to disregard environment variables altogether even in this demo app.

We now need to update the .env file to set these two new values:

REDIS_HOST=redis
REDIS_PORT=6379

Remember earlier how we set the hostname to redis? This was why. If you chose a different name then update accordingly.

As we have added new entries to the .env file we need to reload the stack to make them available:

make dev

# or run the command manually as above

In order to communicate with Redis we will need a Redis Client. I'm going to use Predis. Feel free to use your favourite alternative. However, with ~20m downloads at the time of recording, Predis seems to be the clear favourite for most of us:

docker-compose exec php composer require predis/predis

Now, let's tell Symfony to use Redis for our Cache:

# app/config/config.yml

framework:
    # ...
    cache:
        app: cache.adapter.redis
        default_redis_provider: "redis://%redis.host%:%redis.port%"

We should now be good to use the cache from inside our Symfony application.

Caching Stuff In Redis From Symfony

For our initial example, let's cache a value directly from a controller action:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        $cacheKey = '123';

        $cachedItem = $this->get('cache.app')->getItem($cacheKey);

        if (false === $cachedItem->isHit()) {
            $cachedItem->set($cacheKey, 'some value');
            $this->get('cache.app')->save($cachedItem);
        }

        return $this->render('default/index.html.twig', [
            'cache' => [
                'hit' => $cachedItem->isHit(),
            ]
        ]);
    }
}

To make this most visible, let's amend the default template to the following:

{% extends 'base.html.twig' %}

{% block body %}
Was the cache hit? {{ cache.hit ? 'yes' : 'no' }}
{% endblock %}

It's basic, but it's good enough. We will use the web debug toolbar for a further pry.

Now, if you browse to http://127.0.0.1:81/app_dev.php/ then:

  • On the first refresh you should see that the cache was not hit
  • On the second refresh you should see that the cache was hit

We can validate this in Redis by doing the following:

Firstly, get a terminal connection open to Redis:

docker-compose exec redis /bin/bash

Then once inside Redis, run:

redis-cli

Now, see what keys are set:

127.0.0.1:6379> keys *
1) "fQVL7uG9-N:123"

That's because we just cached a value.

Next, clear the Symfony cache. Do this from your desktop / laptop, not from the Redis CLI:

docker-compose exec php bin/console cache:clear

Now, again from Redis CLI:

127.0.0.1:6379> keys *
(empty list or set)

Now, refresh your browser once again, and you should see the message:

was the cache hit? no

No, it was not hit, as we just cleared the cache.

However, yes, the cache is populated as our controller action will populate the cache if a "cache miss" is detected (i.e. the cache was not hit):

127.0.0.1:6379> keys *
1) "fQVL7uG9-N:123"

Note, it's the exact same key name.

Also, for clarity, everything before the colon is the namespace generated by Symfony behind the scenes. Each of the cache pools, including the default provided pool (cache.app) will have a unique namespace. Everything after the colon is the key name that we provide.

Now, on the subject of key names, I prefer to use md5 to generate them:

    public function indexAction()
    {
        $cacheKey = md5('123');

        $cachedItem = $this->get('cache.app')->getItem($cacheKey);

Which leads to:

127.0.0.1:6379> keys *
1) "fQVL7uG9-N:202cb962ac59075b964b07152d234b70"

Up to you, of course, and it really depends on the complexity of your keys as to whether this is worthwhile.

Now, this is nice and all, but we can do better. So let's keep exploring Symfony and Redis in the very next video.

Code For This Video

Get the code for this video.

Episodes

# Title Duration
1 Get Started With Symfony Cache 03:48
2 [Part 1/2] - Symfony 3 with Redis Cache 07:03
3 [Part 2/2] - Symfony 3 with Redis Cache 08:41