ニュース・ブログ

ニュース・ブログ一覧PHPの記事情報Cache data with Redis in CakePHP 4

Cache data with Redis in CakePHP 4

It’s super easy to cache your DB results in CakePHP with Redis. Actually, it’s so easy it might make you uncomfortable.

Contents

  • Setup Redis in your docker environment
  • Setup Redis in CakePHP
  • Use cache functionality with your query result (DB data)

Setup Redis in the docker environment

I am going to assume the reader has existing knowledge about docker environment and make this section brief.

Installing Redis has two parts :

 

– Setup the Redis server which will host the data
– Install the phpredis extension

Setup the Redis server in the docker-compose.yml file with the following code:

version: '3'
services:
    redis_server:
        image: redis
        volumes:
            - "./redis/db-data:/data"

※ I am going to assume you have also CakePHP and DB setup in there too.

 

Install the phpredis extension in the dockerfile. This should be the same file docker file where you are installing the php.

FROM php:7.4-apache
RUN pecl install redis

If you want to install the Redis not in docker container but in the Linux. You can use this article here.

Setup Redis in CakePHP

In the app.php file inside the cache adapters Cache[]array write the following code :

/*
 * Configure the cache adapters.
 */
'Cache' => [
 'db_data_cache' => [
  'className' => 'Redis',
  'duration' => '+1 hours',
  'host' => 'redis_server',
 ],
]

※ There is already Cache array, so don’t write it again. Just add the array db_data_cache in there.

  • db_data_cache is just a name to refer this config later in the code. You can use any name here you like
  • className here tells CakePHP which caching engine to use. CakePHP supports other cache engine too like Memcache etc.
  • duration is how long the data will be saved in the cache. So after this time has passed new data will be cached for the next 1 hour.
  • host This name is of the service which we have assigned in the docker-compose file earlier.

Use cache functionality with your query result (DB data)

Now with CakePHP built in functionality the caching work just like magic. With your query you can just write the following codes to enable caching for the resultset:

/**
 * If a query has caching enabled, it will do the following when executed:
 * - Check the cache for $key. If there are results no SQL will be executed.
 *   Instead the cached results will be returned.
 * - When the cached data is stale/missing the result set will be cached as the query
 *   is executed.
*/
$query->cache('recent_articles', 'db_data_cache');
  • recent_articles above is the key name which is used to save the data in Redis.
  • db_data_cache is the name of the config which we have setup in app.php file.
  • And cache data will be renewed as the time mentioned in db_data_cache config duration in the app.php file. So in our case it’s in 1hour.

That’s all actually you need to use the cache functionality. And rest of the article is just extra information. Read ahead if you want more ✌🏼

 


You can also generate a key for the cache like this :

$query->cache(function ($q) {
    return 'articles-' . md5(serialize($q->clause('where')));
});

Check whether cache is really working or not

After you have executed the query, you can check whether the data has been cached or not with the following code:

debug( \Cake\Cache\Cache::read('recent_articles', 'db_data_cache') );

 

Remove all the existing cache with the following code:

\Cake\Cache\Cache::clear('db_data_cache');

Learn more in details about the cache in CakePHP documentation:

Bonus

Deciding name for a key could be a bother and specially if you have to cache a lot of places. To solve that I have used the full SQL statement as a key for the query results. Here’s the contents for this bonus section:

  • Get the proper full SQL statement from query
  • Use the SQL statement as a key for the cache.
  • Change the code into a function for easier use in other places

Get the proper full SQL statement from query

Unfortunately in CakePHP there is no built in function (as of CakePHP 4.2) to compile the proper full SQL statement. Although with debugKit you can use the function sql($query) to generate the the SQL statement, but the problem is this only available in with debug mode on. So not on the production environment.

 

To solve this, I have taken the code from the debugKit and added a few lines at the first few lines to make it simpler when using it with the query. Here’s how it looks :

/**
 * Helper function used to replace query placeholders by the real
 * params used to execute the query.
 * 
 * @param query $query Query 
 * @return string return the compiled SQL with the binding value with query
 */
public static function compiledSQL($query)
{
 $sql = $query->sql();
 $bindings = $query->getValueBinder()->bindings();
$params = array_map(function ($binding) {
  $p = $binding['value'];
if ($p === null) {
   return 'NULL';
}elseif (is_bool($p)) {
   return $p ? 'TRUE' : 'FALSE';
}elseif( $p instanceof \DateTimeInterface ){
   return "'".$p->format('Y-m-d H:i:s.u')."'";
}elseif (is_string($p)) {
   $replacements = [
    '$' => '\\$',
    '\\' => '\\\\\\\\',
    "'" => "''",
   ];
$p = strtr($p, $replacements);
return "'$p'";
  }
return $p;
 }, $bindings);
$keys = [];
 $limit = is_int(key($params)) ? 1 : -1;
 foreach ($params as $key => $param) {
  $keys[] = is_string($key) ? "/$key\b/" : '/[?]/';
 }
return preg_replace($keys, $params, $sql, $limit);
}

 

And I have added this static function in a common Utilclass so that I can call it whenever I want.

The Util class resides in the file named Util.php which I have into cake/src/Lib/Util.php . And here’s how it looks inside the file:

<?php
namespace App\Lib;
class Util{
public static function compiledSQL($query)
 {
  // ... 
 }
}

Use the SQL statement as a key for the cache.

This is how you can call the function to generate the SQL statement to use it as a key in the cache:

$query->cache(
 // compiling the sql statement to use it as a key for caching
 function ($query){
  return md5(\App\Lib\Util::compiledSQL($query));
 },
 'db_data_cache'
);

Change the code into a function for easier use in other places

Awesome so far, right? We can compact it more by moving it to a function.

After compacting into a function the code for caching will look like this:

\App\Lib\Util::cacheQueryData($query, 'db_data_cache');

 

And this how is it looks at the Utility class :

/**
 * If a query has caching enabled, it will do the following when executed:
 * - Check the cache for $key. If there are results no SQL will be executed.
 *   Instead the cached results will be returned.
 * - When the cached data is stale/missing the result set will be cached as the query
 *   is executed.
 * 
 * @param query 
 * @param string cache engine config name 
*/
public static function cacheQueryData($query, $cache_config)
{
 $query->cache(
  // compiling the sql statement to use it as a key for caching
  function ($query){
   // This is the function which I have already shared above
   return md5(self::compiledSQL($query));
  },
  $cache_config
 );
}