app/classes/Cache/Cache.php (83 lines of code) (raw):
<?php
declare(strict_types=1);
namespace Cache;
/**
* Cache class
*
* A simple and fast file caching system.
*
* 3 global constants are used: CACHE_ENABLED, CACHE_PATH and CACHE_TIME
* If those app constants are not available, the system temp folder
* and the class variables $CACHE_ENABLED and $CACHE_TIME are used.
*
* @package Cache
*/
class Cache
{
/*
Fallback for activation of Cache
*/
public static bool $CACHE_ENABLED = false;
/*
Cache expiration time (seconds)
*/
public static int $CACHE_TIME = 3600;
/**
* Create a cache file with serialized data
*
* We use PHP serialization, and not other formats like Json for example,
* as it allows storing not only data but also data representations and
* instantiated objects.
*
* @param string $id UID of the cache
* @param mixed $data Data to store
* @param int $ttl Time to live (seconds) for the cached data, defaults to 0
*
* @return bool True if cache file is created
* False if there was an error
*/
public static function setKey(string $id, mixed $data, int $ttl = 0): bool
{
if (! self::isActivated()) {
return false;
}
$immutable = ($ttl === -1) ? true : false;
return file_put_contents(self::getKeyPath($id, $immutable), serialize($data)) ? true : false;
}
/**
* Get the cached serialized data via its UID
*
* @param string $id UID of the cache
* @param int $ttl Number of seconds for time to live.
* Defaults to 0 which calls the default duration. -1 means forever.
*
* @return mixed Unserialized cached data, or false
*/
public static function getKey(string $id, int $ttl = 0): mixed
{
// By default, we cache data that is mutable over time and has an expiry date
$immutable = false;
if (! self::isActivated()) {
return false;
}
if ($ttl === 0) {
$ttl = defined('CACHE_TIME') ? CACHE_TIME : self::$CACHE_TIME;
}
// External immutable data, we keep this data almost forever (30 years here)
if ($ttl === -1) {
$immutable = true;
}
return self::isValidKey($id, $ttl)
? unserialize(file_get_contents(self::getKeyPath($id, $immutable)))
: false;
}
/**
* Flush the current cache
*
* @return bool True if files in cache are deleted
* False if some files were not deleted
*/
public static function flush(): bool
{
$files = glob(self::getCachePath() . '*.cache');
return ! in_array(false, array_map('unlink', $files));
}
/**
* Is the caching system activated?
* We look if the CACHE constant is available and if it's set to True
*
* @return bool True if activated
* False if deactivated
*/
public static function isActivated(): bool
{
// We don't want a global switch for the cache in Unit Tests
// because we want to test functions with and without caching.
if (defined('TESTING_CONTEXT')) {
return self::$CACHE_ENABLED;
}
return defined('CACHE_ENABLED') ? CACHE_ENABLED : self::$CACHE_ENABLED; // @codeCoverageIgnore
}
/**
* Delete a cache file based to its UID
*
* @param string $id UID of the cached data
* @param bool $immutable Is that immutable data? Default to false
*
* @return bool True if data was deleted
* False if it doesn't exist
*/
public static function deleteKey(string $id, bool $immutable = false): bool
{
$file = self::getKeyPath($id, $immutable);
// Make sure this wasn't already deleted and the server file cache is lying
clearstatcache(true, $file);
if (! file_exists($file)) {
return false;
}
// if there is a lock on the file, we can't delete it,
// Can happen if it's being created by another php process
if (! is_writable($file)) {
return false;
}
return @unlink($file);
}
/**
* Get the path to the cached file
*
* Filename is in the form a840d513be5240045ccc979208f739a168946332.cache
* Immutable cached files are in the form a840d513be5240045ccc979208f739a168946332.immutable
*
* @param string $id UID of the cached file
* @param bool $immutable is that immutable data? Default to false
*
* @return string Path to the file
*/
public static function getKeyPath(string $id, bool $immutable = false): string
{
return self::getCachePath() . sha1($id) . ($immutable ? '.immutable' : '.cache');
}
/**
* Get the path to the cache folder
*
* Use a CACHE_PATH global constant if defined, otherwise use OS
* default folder for temporary files.
*
* @return string Path to cache folder
*/
public static function getCachePath(): string
{
return defined('CACHE_PATH') ? CACHE_PATH : sys_get_temp_dir() . '/';
}
/**
* Check if cached data for a key is usable
*
* @param string $id UID for the data
* @param int $ttl Number of seconds for time to live
*
* @return bool True if valid data
* False if cached data is not usable
*/
private static function isValidKey(string $id, int $ttl): bool
{
$immutable = ($ttl === -1) ? true : false;
// No cache file
if (! file_exists(self::getKeyPath($id, $immutable))) {
return false;
}
// Cache is obsolete and was deleted
if (self::isObsoleteKey($id, $ttl)) {
self::deleteKey($id);
return false;
}
// All good, cache is valid
return true;
}
/**
* Check if the data has not expired
*
* @param string $id UID of the cached file
* @param int $ttl Number of seconds for time to live
*
* @return bool True if file is obsolete
* False if it is still usable
*/
private static function isObsoleteKey(string $id, int $ttl): bool
{
// Immutable data is never obsolete
if ($ttl === -1) {
return false;
}
return filemtime(self::getKeyPath($id)) < time() - $ttl;
}
}