You don’t need expansive libraries for simple operations such as disk caching if you’re only using the basic features of just saving, retrieving and deleting data from a file. See below for a reusable PHP trait that can be incorporated easily into your existing code.
It will hash the cache key so that each file is unique on disk, as well as storing the actual data in JSON format inside the file. There’s a method to init the disk cache, creating the directory if it doesn’t already exist. Saving the cache to disk also includes the lifetime of the data you save, meaning that each cache entry you safe can have a different lifetime.
<?php
trait DiskCacheTrait {
private string $storagePath;
/**
* Initialize the disk cache.
*
* @param string $storageDir The directory where cache files are stored.
*/
public function initDiskCache(string $storageDir = __DIR__ . '/cache_storage'): void {
$this->storagePath = rtrim($storageDir, '/') . '/';
if (!is_dir($this->storagePath)) {
mkdir($this->storagePath, 0755, true);
}
}
/**
* Save data to the disk cache.
*
* @param string $cacheKey A unique identifier for the cached item.
* @param mixed $content The data to be cached.
* @param int $lifetime Duration in seconds before the cache expires.
* @return bool Returns true if the cache was successfully saved.
*/
public function putCache(string $cacheKey, mixed $content, int $lifetime = 3600): bool {
$filename = $this->makeCacheFileName($cacheKey);
$cacheEntry = [
'expires_at' => time() + $lifetime,
'payload' => $content
];
$jsonData = json_encode($cacheEntry, JSON_THROW_ON_ERROR);
return file_put_contents($filename, $jsonData, LOCK_EX) !== false;
}
/**
* Retrieve data from the disk cache.
*
* @param string $cacheKey A unique identifier for the cached item.
* @return mixed|null Returns the cached data, or null if not found or expired.
*/
public function fetchCache(string $cacheKey): mixed {
$filename = $this->makeCacheFileName($cacheKey);
if (!is_file($filename) || !is_readable($filename)) {
return null;
}
$cacheEntry = json_decode(file_get_contents($filename), true, 512, JSON_THROW_ON_ERROR);
if ($cacheEntry['expires_at'] < time()) {
$this->removeCache($cacheKey);
return null;
}
return $cacheEntry['payload'];
}
/**
* Remove a cache entry.
*
* @param string $cacheKey A unique identifier for the cached item.
* @return bool Returns true if the cache file was successfully deleted.
*/
public function removeCache(string $cacheKey): bool {
$filename = $this->makeCacheFileName($cacheKey);
return is_file($filename) ? unlink($filename) : false;
}
/**
* Generate a filename based on the cache key.
*
* @param string $cacheKey A unique identifier for the cached item.
* @return string The generated file name for storing the cache.
*/
private function makeCacheFileName(string $cacheKey): string {
return $this->storagePath . hash('sha256', $cacheKey) . '.cache';
}
}
Example usage of this trait would be something like the following code block, remember that traits are designed to be used specifically inside of classes, you can turn the code above into a regular class if you need to use it outside of a class.
class CacheManager {
use DiskCacheTrait;
public function __construct() {
$this->initDiskCache(); // Use default cache directory
}
public function saveSetting(string $key, mixed $value): bool {
return $this->putCache($key, $value);
}
public function getSetting(string $key): mixed {
return $this->fetchCache($key);
}
public function deleteSetting(string $key): bool {
return $this->removeCache($key);
}
}
// Example usage
$cache = new CacheManager();
// Save a setting
$cache->saveSetting('site_name', 'My Awesome Site');
// Retrieve a setting
$siteName = $cache->getSetting('site_name');
if ($siteName !== null) {
echo "Site Name: " . $siteName;
} else {
echo "Setting not found or expired.";
}
As you can see above, we’re written a wrapper around the trait so that we can use it anywhere. The main logic for actual writing to disk and checking for expiry, filename generation etc. is inside of the trait.
This makes the wrapper class much cleaner to look at, of course if you’re integrating this directly into one of your classes then the ‘wrapper’ above wouldn’t actually be needed.