The cache software design pattern is where you store a result that takes a long time to generate. This could take the form of a lengthy view script or a complex database query. The storage destination needs to be highly performant, of course, if you wish to improve the user experience of website visitors. As different installations will have different potential storage targets, the cache mechanism lends itself to the adapter pattern as well. Examples of potential storage destinations include memory, a database, and the filesystem.
Application\Cache\Constants class:<?php
namespace Application\Cache;
class Constants
{
const DEFAULT_GROUP = 'default';
const DEFAULT_PREFIX = 'CACHE_';
const DEFAULT_SUFFIX = '.cache';
const ERROR_GET = 'ERROR: unable to retrieve from cache';
// not all constants are shown to conserve space
}namespace Application\Cache;
interface CacheAdapterInterface
{
public function hasKey($key);
public function getFromCache($key, $group);
public function saveToCache($key, $data, $group);
public function removeByKey($key);
public function removeByGroup($group);
}namespace Application\Cache;
use PDO;
use Application\Database\Connection;
class Database implements CacheAdapterInterface
{
protected $sql;
protected $connection;
protected $table;
protected $dataColumnName;
protected $keyColumnName;
protected $groupColumnName;
protected $statementHasKey = NULL;
protected $statementGetFromCache = NULL;
protected $statementSaveToCache = NULL;
protected $statementRemoveByKey = NULL;
protected $statementRemoveByGroup= NULL;Application\Database\Connection instance and the name of the table used for the cache:public function __construct(Connection $connection,
$table,
$idColumnName,
$keyColumnName,
$dataColumnName,
$groupColumnName = Constants::DEFAULT_GROUP)
{
$this->connection = $connection;
$this->setTable($table);
$this->setIdColumnName($idColumnName);
$this->setDataColumnName($dataColumnName);
$this->setKeyColumnName($keyColumnName);
$this->setGroupColumnName($groupColumnName);
}public function prepareHasKey()
{
$sql = 'SELECT `' . $this->idColumnName . '` '
. 'FROM `' . $this->table . '` '
. 'WHERE `' . $this->keyColumnName . '` = :key ';
$this->sql[__METHOD__] = $sql;
$this->statementHasKey =
$this->connection->pdo->prepare($sql);
}
public function prepareGetFromCache()
{
$sql = 'SELECT `' . $this->dataColumnName . '` '
. 'FROM `' . $this->table . '` '
. 'WHERE `' . $this->keyColumnName . '` = :key '
. 'AND `' . $this->groupColumnName . '` = :group';
$this->sql[__METHOD__] = $sql;
$this->statementGetFromCache =
$this->connection->pdo->prepare($sql);
}public function hasKey($key)
{
$result = 0;
try {
if (!$this->statementHasKey) $this->prepareHasKey();
$this->statementHasKey->execute(['key' => $key]);
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(Constants::ERROR_REMOVE_KEY);
}
return (int) $this->statementHasKey
->fetch(PDO::FETCH_ASSOC)[$this->idColumnName];
}SELECT, with a WHERE clause, which incorporates the key and group:public function getFromCache(
$key, $group = Constants::DEFAULT_GROUP)
{
try {
if (!$this->statementGetFromCache)
$this->prepareGetFromCache();
$this->statementGetFromCache->execute(
['key' => $key, 'group' => $group]);
while ($row = $this->statementGetFromCache
->fetch(PDO::FETCH_ASSOC)) {
if ($row && count($row)) {
yield unserialize($row[$this->dataColumnName]);
}
}
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(Constants::ERROR_GET);
}
}UPDATE; otherwise, we perform an INSERT:public function saveToCache($key, $data, $group = Constants::DEFAULT_GROUP)
{
$id = $this->hasKey($key);
$result = 0;
try {
if ($id) {
if (!$this->statementUpdateCache)
$this->prepareUpdateCache();
$result = $this->statementUpdateCache
->execute(['key' => $key,
'data' => serialize($data),
'group' => $group,
'id' => $id]);
} else {
if (!$this->statementSaveToCache)
$this->prepareSaveToCache();
$result = $this->statementSaveToCache
->execute(['key' => $key,
'data' => serialize($data),
'group' => $group]);
}
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(Constants::ERROR_SAVE);
}
return $result;
}public function removeByKey($key)
{
$result = 0;
try {
if (!$this->statementRemoveByKey)
$this->prepareRemoveByKey();
$result = $this->statementRemoveByKey->execute(
['key' => $key]);
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(Constants::ERROR_REMOVE_KEY);
}
return $result;
}
public function removeByGroup($group)
{
$result = 0;
try {
if (!$this->statementRemoveByGroup)
$this->prepareRemoveByGroup();
$result = $this->statementRemoveByGroup->execute(
['group' => $group]);
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(Constants::ERROR_REMOVE_GROUP);
}
return $result;
}public function setTable($name)
{
$this->table = $name;
}
public function getTable()
{
return $this->table;
}
// etc.
}md5(), not for security, but as a way of quickly generating a text string from the key:namespace Application\Cache;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
class File implements CacheAdapterInterface
{
protected $dir;
protected $prefix;
protected $suffix;
public function __construct(
$dir, $prefix = NULL, $suffix = NULL)
{
if (!file_exists($dir)) {
error_log(__METHOD__ . ':' . Constants::ERROR_DIR_NOT);
throw new Exception(Constants::ERROR_DIR_NOT);
}
$this->dir = $dir;
$this->prefix = $prefix ?? Constants::DEFAULT_PREFIX;
$this->suffix = $suffix ?? Constants::DEFAULT_SUFFIX;
}
public function hasKey($key)
{
$action = function ($name, $md5Key, &$item) {
if (strpos($name, $md5Key) !== FALSE) {
$item ++;
}
};
return $this->findKey($key, $action);
}
public function getFromCache($key, $group = Constants::DEFAULT_GROUP)
{
$fn = $this->dir . '/' . $group . '/'
. $this->prefix . md5($key) . $this->suffix;
if (file_exists($fn)) {
foreach (file($fn) as $line) { yield $line; }
} else {
return array();
}
}
public function saveToCache(
$key, $data, $group = Constants::DEFAULT_GROUP)
{
$baseDir = $this->dir . '/' . $group;
if (!file_exists($baseDir)) mkdir($baseDir);
$fn = $baseDir . '/' . $this->prefix . md5($key)
. $this->suffix;
return file_put_contents($fn, json_encode($data));
}
protected function findKey($key, callable $action)
{
$md5Key = md5($key);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->dir),
RecursiveIteratorIterator::SELF_FIRST);
$item = 0;
foreach ($iterator as $name => $obj) {
$action($name, $md5Key, $item);
}
return $item;
}
public function removeByKey($key)
{
$action = function ($name, $md5Key, &$item) {
if (strpos($name, $md5Key) !== FALSE) {
unlink($name);
$item++;
}
};
return $this->findKey($key, $action);
}
public function removeByGroup($group)
{
$removed = 0;
$baseDir = $this->dir . '/' . $group;
$pattern = $baseDir . '/' . $this->prefix . '*'
. $this->suffix;
foreach (glob($pattern) as $file) {
unlink($file);
$removed++;
}
return $removed;
}
}CacheAdapterInterface as an argument:namespace Application\Cache;
use Psr\Http\Message\RequestInterface;
use Application\MiddleWare\ { Request, Response, TextStream };
class Core
{
public function __construct(CacheAdapterInterface $adapter)
{
$this->adapter = $adapter;
}Psr\Http\Message\RequestInterface class an an argument, and return a Psr\Http\Message\ResponseInterface as a response. We start with a simple one: hasKey(). Note how we extract the key from the request parameters:public function hasKey(RequestInterface $request)
{
$key = $request->getUri()->getQueryParams()['key'] ?? '';
$result = $this->adapter->hasKey($key);
}204 code, which indicates the request was a success, but no content was produced. Otherwise, we set a 200 (success) code, and iterate through the results. Everything is then stuffed into a response object, which is returned:public function getFromCache(RequestInterface $request)
{
$text = array();
$key = $request->getUri()->getQueryParams()['key'] ?? '';
$group = $request->getUri()->getQueryParams()['group']
?? Constants::DEFAULT_GROUP;
$results = $this->adapter->getFromCache($key, $group);
if (!$results) {
$code = 204;
} else {
$code = 200;
foreach ($results as $line) $text[] = $line;
}
if (!$text || count($text) == 0) $code = 204;
$body = new TextStream(json_encode($text));
return (new Response())->withStatus($code)
->withBody($body);
}public function saveToCache(RequestInterface $request)
{
$text = array();
$key = $request->getUri()->getQueryParams()['key'] ?? '';
$group = $request->getUri()->getQueryParams()['group']
?? Constants::DEFAULT_GROUP;
$data = $request->getBody()->getContents();
$results = $this->adapter->saveToCache($key, $data, $group);
if (!$results) {
$code = 204;
} else {
$code = 200;
$text[] = $results;
}
$body = new TextStream(json_encode($text));
return (new Response())->withStatus($code)
->withBody($body);
}public function removeByKey(RequestInterface $request)
{
$text = array();
$key = $request->getUri()->getQueryParams()['key'] ?? '';
$results = $this->adapter->removeByKey($key);
if (!$results) {
$code = 204;
} else {
$code = 200;
$text[] = $results;
}
$body = new TextStream(json_encode($text));
return (new Response())->withStatus($code)
->withBody($body);
}
public function removeByGroup(RequestInterface $request)
{
$text = array();
$group = $request->getUri()->getQueryParams()['group']
?? Constants::DEFAULT_GROUP;
$results = $this->adapter->removeByGroup($group);
if (!$results) {
$code = 204;
} else {
$code = 200;
$text[] = $results;
}
$body = new TextStream(json_encode($text));
return (new Response())->withStatus($code)
->withBody($body);
}
} // closing brace for class CoreIn order to demonstrate the use of the Acl class, you will need to define the classes described in this recipe, summarized here:
|
Class |
Discussed in these steps |
|---|---|
|
|
1 |
|
|
2 |
|
|
3 - 10 |
|
|
11 |
|
|
12 - 16 |
Next, define a test program, which you could call chap_09_middleware_cache_db.php. In this program, as usual, define constants for necessary files, set up autoloading, use the appropriate classes, oh... and write a function that produces prime numbers (you're probably re-reading that last little bit at this point. Not to worry, we can help you with that!):
<?php
define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
define('DB_TABLE', 'cache');
define('CACHE_DIR', __DIR__ . '/cache');
define('MAX_NUM', 100000);
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
use Application\Cache\{ Constants, Core, Database, File };
use Application\MiddleWare\ { Request, TextStream };Well, a function that takes a long time to run is needed, so prime number generator, here we go! The numbers 1, 2, and 3 are given as primes. We use the PHP 7 yield from syntax to produce these first three. then, we skip right to 5, and proceed up to the maximum value requested:
function generatePrimes($max)
{
yield from [1,2,3];
for ($x = 5; $x < $max; $x++)
{
if($x & 1) {
$prime = TRUE;
for($i = 3; $i < $x; $i++) {
if(($x % $i) === 0) {
$prime = FALSE;
break;
}
}
if ($prime) yield $x;
}
}
}You can then set up a database cache adapter instance, which serves as an argument for the core:
$conn = new Connection(include DB_CONFIG_FILE); $dbCache = new Database( $conn, DB_TABLE, 'id', 'key', 'data', 'group'); $core = new Core($dbCache);
Alternatively, if you wish to use the file cache adapter instead, here is the appropriate code:
$fileCache = new File(CACHE_DIR); $core = new Core($fileCache);
If you wanted to clear the cache, here is how it might be done:
$uriString = '/?group=' . Constants::DEFAULT_GROUP; $cacheRequest = new Request($uriString, 'get'); $response = $core->removeByGroup($cacheRequest);
You can use time() and microtime() to see how long this script runs with and without the cache:
$start = time() + microtime(TRUE); echo "\nTime: " . $start;
Next, generate a cache request. A status code of 200 indicates you were able to obtain a list of primes from the cache:
$uriString = '/?key=Test1';
$cacheRequest = new Request($uriString, 'get');
$response = $core->getFromCache($cacheRequest);
$status = $response->getStatusCode();
if ($status == 200) {
$primes = json_decode($response->getBody()->getContents());Otherwise, you can assume nothing was obtained from the cache, which means you need to generate prime numbers, and save the results to the cache:
} else {
$primes = array();
foreach (generatePrimes(MAX_NUM) as $num) {
$primes[] = $num;
}
$body = new TextStream(json_encode($primes));
$response = $core->saveToCache(
$cacheRequest->withBody($body));
}You can then check the stop time, calculate the difference, and have a look at your new list of primes:
$time = time() + microtime(TRUE); $diff = $time - $start; echo "\nTime: $time"; echo "\nDifference: $diff"; var_dump($primes);
Here is the expected output before values were stored in the cache:

You can now run the same program again, this time retrieving from the cache:

Allowing for the fact that our little prime number generator is not the world's most efficient, and also that the demonstration was run on a laptop, the time went from over 30 seconds down to milliseconds.
Another possible cache adapter could be built around commands that are part of the Alternate PHP Cache (APC) extension. This extension includes such functions as apc_exists(), apc_store(), apc_fetch(), and apc_clear_cache(). These functions are perfect for our hasKey(), saveToCache(), getFromCache(), and removeBy*() functions.
You might consider making slight changes to the cache adapter classes described previously following PSR-6, which is a standards recommendation directed towards the cache. There is not the same level of acceptance of this standard as with PSR-7, however, so we decided to not follow this standard exactly in the recipe presented here. For more information on PSR-6, please refer to http://www.php-fig.org/psr/psr-6/.