309 lines
8.0 KiB
PHP
309 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace SleekDB;
|
|
|
|
use Closure;
|
|
use Exception;
|
|
use ReflectionFunction;
|
|
use SleekDB\Classes\IoHelper;
|
|
use SleekDB\Exceptions\IOException;
|
|
|
|
/**
|
|
* Class Cache
|
|
* Caching layer of SleekDB, handles everything regarding caching.
|
|
*/
|
|
class Cache
|
|
{
|
|
|
|
const DEFAULT_CACHE_DIR = "cache/";
|
|
const NO_LIFETIME_FILE_STRING = "no_lifetime";
|
|
|
|
/**
|
|
* Lifetime in seconds or deletion with deleteAll
|
|
* @var int|null
|
|
*/
|
|
protected $lifetime;
|
|
|
|
protected $cachePath = "";
|
|
|
|
protected $cacheDir = "";
|
|
|
|
protected $tokenArray;
|
|
|
|
/**
|
|
* Cache constructor.
|
|
* @param string $storePath
|
|
* @param array $cacheTokenArray
|
|
* @param int|null $cacheLifetime
|
|
*/
|
|
public function __construct(string $storePath, array &$cacheTokenArray, $cacheLifetime)
|
|
{
|
|
// TODO make it possible to define custom cache directory.
|
|
// $cacheDir = "";
|
|
// $this->setCacheDir($cacheDir);
|
|
|
|
$this->setCachePath($storePath);
|
|
|
|
$this->setTokenArray($cacheTokenArray);
|
|
|
|
$this->lifetime = $cacheLifetime;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the cache lifetime for current query.
|
|
* @return int|null lifetime in seconds (int) or no lifetime with null
|
|
*/
|
|
public function getLifetime()
|
|
{
|
|
return $this->lifetime;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the path to cache folder of current store.
|
|
* @return string path to cache directory
|
|
*/
|
|
public function getCachePath(): string
|
|
{
|
|
return $this->cachePath;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the cache token used as filename to store cache file.
|
|
* @return string unique token for current query.
|
|
*/
|
|
public function getToken(): string
|
|
{
|
|
$tokenArray = $this->getTokenArray();
|
|
$tokenArray = self::convertClosuresToString($tokenArray);
|
|
|
|
return md5(json_encode($tokenArray));
|
|
}
|
|
|
|
/**
|
|
* Delete all cache files for current store.
|
|
* @return bool
|
|
*/
|
|
public function deleteAll(): bool
|
|
{
|
|
return IoHelper::deleteFiles(glob($this->getCachePath()."*"));
|
|
}
|
|
|
|
/**
|
|
* Delete all cache files with no lifetime (null) in current store.
|
|
* @return bool
|
|
*/
|
|
public function deleteAllWithNoLifetime(): bool
|
|
{
|
|
$noLifetimeFileString = self::NO_LIFETIME_FILE_STRING;
|
|
return IoHelper::deleteFiles(glob($this->getCachePath()."*.$noLifetimeFileString.json"));
|
|
}
|
|
|
|
/**
|
|
* Save content for current query as a cache file.
|
|
* @param array $content
|
|
* @throws IOException if cache folder is not writable or saving failed.
|
|
*/
|
|
public function set(array $content){
|
|
$lifetime = $this->getLifetime();
|
|
$cachePath = $this->getCachePath();
|
|
$token = $this->getToken();
|
|
|
|
$noLifetimeFileString = self::NO_LIFETIME_FILE_STRING;
|
|
$cacheFile = $cachePath . $token . ".$noLifetimeFileString.json";
|
|
|
|
if(is_int($lifetime)){
|
|
$cacheFile = $cachePath . $token . ".$lifetime.json";
|
|
}
|
|
|
|
IoHelper::writeContentToFile($cacheFile, json_encode($content));
|
|
}
|
|
|
|
/**
|
|
* Retrieve content of cache file.
|
|
* @return array|null array on success, else null
|
|
* @throws IOException if cache file is not readable or does not exist.
|
|
*/
|
|
public function get(){
|
|
$cachePath = $this->getCachePath();
|
|
$token = $this->getToken();
|
|
|
|
$cacheFile = null;
|
|
|
|
IoHelper::checkRead($cachePath);
|
|
|
|
$cacheFiles = glob($cachePath.$token."*.json");
|
|
|
|
if($cacheFiles !== false && count($cacheFiles) > 0){
|
|
$cacheFile = $cacheFiles[0];
|
|
}
|
|
|
|
if(!empty($cacheFile)){
|
|
$cacheParts = explode(".", $cacheFile);
|
|
if(count($cacheParts) >= 3){
|
|
$lifetime = $cacheParts[count($cacheParts) - 2];
|
|
if(is_numeric($lifetime)){
|
|
if($lifetime === "0"){
|
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
|
}
|
|
$fileExpiredAfter = filemtime($cacheFile) + (int) $lifetime;
|
|
if(time() <= $fileExpiredAfter){
|
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
|
}
|
|
IoHelper::deleteFile($cacheFile);
|
|
} else if($lifetime === self::NO_LIFETIME_FILE_STRING){
|
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Delete cache file/s for current query.
|
|
* @return bool
|
|
*/
|
|
public function delete(): bool
|
|
{
|
|
return IoHelper::deleteFiles(glob($this->getCachePath().$this->getToken()."*.json"));
|
|
}
|
|
|
|
/**
|
|
* @param string $storePath
|
|
* @return Cache
|
|
*/
|
|
private function setCachePath(string $storePath): Cache
|
|
{
|
|
$cachePath = "";
|
|
$cacheDir = $this->getCacheDir();
|
|
|
|
if(!empty($storePath)){
|
|
IoHelper::normalizeDirectory($storePath);
|
|
$cachePath = $storePath . $cacheDir;
|
|
}
|
|
|
|
$this->cachePath = $cachePath;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the cache token array used for cache token string generation.
|
|
* @param array $tokenArray
|
|
* @return Cache
|
|
*/
|
|
private function setTokenArray(array &$tokenArray): Cache
|
|
{
|
|
$this->tokenArray = &$tokenArray;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the cache token array.
|
|
* @return array
|
|
*/
|
|
private function getTokenArray(): array
|
|
{
|
|
return $this->tokenArray;
|
|
}
|
|
|
|
/**
|
|
* Convert one or multiple closures to string. If array provided, recursively.
|
|
* @param mixed $data
|
|
* @return mixed
|
|
*/
|
|
private static function convertClosuresToString($data){
|
|
if(!is_array($data)){
|
|
if($data instanceof \Closure){
|
|
return self::getClosureAsString($data);
|
|
}
|
|
return $data;
|
|
}
|
|
foreach ($data as $key => $token){
|
|
if(is_array($token)){
|
|
$data[$key] = self::convertClosuresToString($token);
|
|
} else if($token instanceof \Closure){
|
|
$data[$key] = self::getClosureAsString($token);
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a string representation of a closure that can be used to differentiate between closures
|
|
* when generating the cache token string.
|
|
* @param Closure $closure
|
|
* @return false|string string representation of closure or false on failure.
|
|
*/
|
|
private static function getClosureAsString(Closure $closure)
|
|
{
|
|
try{
|
|
$reflectionFunction = new ReflectionFunction($closure); // get reflection object
|
|
} catch (Exception $exception){
|
|
return false;
|
|
}
|
|
$filePath = $reflectionFunction->getFileName(); // absolute path of php file containing function
|
|
$startLine = $reflectionFunction->getStartLine(); // start line of function
|
|
$endLine = $reflectionFunction->getEndLine(); // end line of function
|
|
$lineSeparator = PHP_EOL; // line separator "\n"
|
|
|
|
$staticVariables = $reflectionFunction->getStaticVariables();
|
|
$staticVariables = var_export($staticVariables, true);
|
|
|
|
if($filePath === false || $startLine === false || $endLine === false){
|
|
return false;
|
|
}
|
|
|
|
$startEndDifference = $endLine - $startLine;
|
|
|
|
$startLine--; // -1 to use it with the array representation of the file
|
|
|
|
if($startLine < 0 || $startEndDifference < 0){
|
|
return false;
|
|
}
|
|
|
|
// get content of file containing function
|
|
$fp = fopen($filePath, 'rb');
|
|
$fileContent = "";
|
|
if(flock($fp, LOCK_SH)){
|
|
$fileContent = @stream_get_contents($fp);
|
|
}
|
|
flock($fp, LOCK_UN);
|
|
fclose($fp);
|
|
|
|
if(empty($fileContent)){
|
|
return false;
|
|
}
|
|
|
|
// separate the file into an array containing every line as one element
|
|
$fileContentArray = explode($lineSeparator, $fileContent);
|
|
if(count($fileContentArray) < $endLine){
|
|
return false;
|
|
}
|
|
|
|
// return the part of the file containing the function as a string.
|
|
$functionString = implode("", array_slice($fileContentArray, $startLine, $startEndDifference + 1));
|
|
$functionString .= "|staticScopeVariables:".$staticVariables;
|
|
return $functionString;
|
|
}
|
|
|
|
/**
|
|
* Set the cache directory name.
|
|
* @param string $cacheDir
|
|
* @return Cache
|
|
*/
|
|
private function setCacheDir(string $cacheDir): Cache
|
|
{
|
|
IoHelper::normalizeDirectory($cacheDir);
|
|
$this->cacheDir = $cacheDir;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the cache directory name or the default cache directory name if empty.
|
|
* @return string
|
|
*/
|
|
private function getCacheDir(): string
|
|
{
|
|
return (!empty($this->cacheDir)) ? $this->cacheDir : self::DEFAULT_CACHE_DIR;
|
|
}
|
|
} |