This site looks a lot better with CSS turned on. Note: IE<10 is not supported.

an article is displayed.

Symfony 4: caching in memory and with Redis

Using Redis as a cache inside Symfony 4

Introduction

I've been writing a lot of PHP over the last few months, and have been getting a decent amount of experience with the Symfony framework. In my Symfony 4 project, I wanted to swap out the default memory caching for a Redis database, and thought that it might be helpful to others if I shared my solution.

I've created a Github repo for anyone that wants a quick working example, but remember that you'll still need to configure a Redis instance if you want to go beyond in-memory caching.

This is only a very simple example, but it should illustrate the basic steps that I took to get caching up and running in Symfony; both in memory and with Redis. I'm only just starting out with PHP, so do your own research too.

Goals

By the end of this article, you should be able to:

  • set up a skeleton Symfony 4 project
  • use Symfony's default in-memory caching
  • configure Symfony to use Redis caching

Prerequisites

  • OSX Mojave
  • A local installation of the Composer dependency manager
  • A running instance of Redis available either locally or at a remote location
  • Basic knowledge of PHP and the Symfony framework

OSX Mojave isn't an essential requirement, but that's what I've used to test everything.

Create a Symfony 4 skeleton project

If you're unsure about this process then please read my article that explains how to create a new Symfony project from scratch.

For our purposes here, I'm using symfony_4_redis_cache for the new project name, so if you wish to follow along exactly then be sure to use this instead of the name that I use in the other article.

Create a new Controller

We'll need a controller to deal with requests to our new Symfony application.

Open up the new project in your favourite code editor

Create the file src/Controller/HomeController.php, and paste in the following code:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Routing\Annotation\Route;
use DateInterval;

class HomeController extends AbstractController
{
    public function __construct(AdapterInterface $cache)
    {
        $this->cache = $cache;
    }

    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        $cacheKey = 'thisIsACacheKey';
        $item = $this->cache->getItem($cacheKey);

        $itemCameFromCache = true;
        if (!$item->isHit()) {
            $itemCameFromCache = false;
            $item->set('this is some data to cache');
            $item->expiresAfter(new DateInterval('PT10S')); // the item will be cached for 10 seconds
            $this->cache->save($item);

        }

        return $this->render('home/index.html.twig', ['isCached' => $itemCameFromCache ? 'true' : 'false']);
    }
}

Let's have a look at what's going on in that code.

The first line of interest involves the AdapterInterface:

use Symfony\Component\Cache\Adapter\AdapterInterface;

A quick look at the Symfony 4 documentation confirms that the Adapter Interface is used as a blueprint for "adapters managing instances of Symfony's CacheItem.", and that Cache Items "are the information units stored in the cache as a key/value pair".

The super useful auto-wiring functionality available in Symfony 4 allows us to inject AdapterInterface as a dependency, simple by requiring it as a constructor parameter:

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

We then create a cache key, and check whether there is any data stored against it in the cache:

$cacheKey = 'thisIsACacheKey';
$item = $this->cache->getItem($cacheKey);

If the item doesn't already exist in the cache.......

if (!$item->isHit()) {

.......we add some data to the cache, set an expiry time for the item and then save the item to the cache:

$item->set('this is some data to cache');
$item->expiresAfter(new DateInterval('PT10S')); // the item will be cached for 10 seconds
$this->cache->save($item);

Depending on how we set the $itemCameFromCache variable, we then send a true or false string to a Twig template:

return $this->render('home/index.html.twig', ['isCached' => $itemCameFromCache ? 'true' : 'false']);

Create a Twig template

The Twig template that we're referencing in the code above doesn't even exist yet, so let's create it.

Create the file templates/home/index.html.twig, and paste in the following code:

{% extends 'base.html.twig' %}
{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Caching with Symfony 4</h1>
    <p>The cache was hit: {{ isCached }}</p>
</div>
{% endblock %}

Testing the in-memory caching

Refresh your browser, and you should see a message informing you that the most recent request did not hit the cache.

The in-memory cache was not hit

Refresh your browser again (within the 10 second cache limit that we set), and you should see a message informing you that the most recent request hit the cache.

The in-memory cache was hit

You should see that the item we chose to cache is saved for 10 seconds before being cleared.

If you wish to verify this further, click on the cache icon in Symfony's "sticky bar" that appears at the bottom of the page, and take a look at how the cache is being used.

Look in the Calls section, and you should see the entry for the thisIsACacheKey key that we stored.

Symfony's sticky bar

Using Redis caching instead

Before we can use Redis caching, we need to install a Redis client for PHP.

The one I'm going to use here is called Predis.

Run the following command to install it using Composer.

composer require predis/predis

You'll also need to update the cache section of your config/packages/cache.yaml file as follows:

cache:
    # Put the unique name of your app here: the prefix seed
    # is used to compute stable namespaces for cache keys.
    prefix_seed: hackerbox_io/symfony_4_redis_cache

    # The app cache caches to the filesystem by default.
    # Other options include:

    # Redis
    app: cache.adapter.redis
    default_redis_provider: redis://redis:6379

Let's take a closer look at that.

As the comment indicates, the prefix_seed value is used to create namespaces for cache keys. Use a name that you feel is appropriate here.

    prefix_seed: hackerbox_io/symfony_4_redis_cache

The app value tells Symfony which adapter to use:

    app: cache.adapter.redis

And default_redis_provider is the reference to your Redis instance:

    default_redis_provider: redis://redis:6379

I'm running an instance of Redis inside a Docker container, and I've created an entry in my hosts file that will point the redis domain at 127.0.0.1. The port 6379 will probably be the same for you, as this is the default, but you'll need to alter the rest of the url to meet your own needs. e.g. redis://<your url here>:6379

Make the changes described above, and restart your application.

Testing the Redis caching

The first part of this is pretty much identical to the steps we took to test the in-memory caching, but I'll reiterate the process to make for easier reading.

Refresh your browser, and you should see a message informing you that the most recent request did not hit the cache.

The in-memory cache was not hit

Refresh your browser again (within the 10 second cache limit that we set), and you should see a message informing you that the most recent request hit the cache.

The in-memory cache was hit

You should see that the item we chose to cache is saved for 10 seconds before being cleared.

If you wish to verify this further, click on the cache icon in Symfony's "sticky bar" that appears at the bottom of the page, and take a look at how the cache is being used.

Symfony's sticky bar

Look in the Calls section, and you should see the entry for the thisIsACacheKey key that we stored.

We can further confirm that Redis has stored our cached value by searching for the thisIsACacheKey key in the database itself.

You'll need to connect to your Redis instance and then search for the key, remembering of course that it will only exist for 10 seconds if you've following this tutorial exactly.

The exact process for connection will depend on how you have Redis set up, but here are the steps that I use when connecting to my local instance (which runs as a Docker container) using the redis-cli tool:

Initiate the connection:

    redis-cli -p 6379

Port 6379 is actually just the default, so you might not need to specify that.

When the connection has been established, list all of the stored keys:

    keys *

You should see something similar to:

    1) "ATVjoFtyo0:thisIsACacheKey"

If you can't see the key in the list, then make sure that you're searching within the key's expiry time (10 seconds in our case).

Summary

So that's how I managed to get in-memory and Redis caching working for Symfony 4. I'm only quite new to PHP and the Symfony framework, but whilst there are probably better ways to achieve the same goal, there'll hopefully be enough in this tutorial to get you off the starting blocks.

Be sure to do your own research before you use any of this stuff in production, and please share my article around if you find it useful.

If you liked that article, then why not try:

Setting up Postgres inside a Docker container

I've been doing a bit of work with Postgres recently, and encountered a few gotchas whilst getting things set up inside a Docker container for the first time. I thought I'd share my final process here, just in case it comes in handy for the equally uninitiated.