file_cache_준비
This commit is contained in:
+353
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Implements the cache for a application.
|
||||
*/
|
||||
class Cache
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** dependency */
|
||||
public const
|
||||
PRIORITY = 'priority',
|
||||
EXPIRATION = 'expire',
|
||||
EXPIRE = 'expire',
|
||||
SLIDING = 'sliding',
|
||||
TAGS = 'tags',
|
||||
FILES = 'files',
|
||||
ITEMS = 'items',
|
||||
CONSTS = 'consts',
|
||||
CALLBACKS = 'callbacks',
|
||||
NAMESPACES = 'namespaces',
|
||||
ALL = 'all';
|
||||
|
||||
/** @internal */
|
||||
public const NAMESPACE_SEPARATOR = "\x00";
|
||||
|
||||
/** @var IStorage */
|
||||
private $storage;
|
||||
|
||||
/** @var string */
|
||||
private $namespace;
|
||||
|
||||
|
||||
public function __construct(IStorage $storage, string $namespace = null)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
$this->namespace = $namespace . self::NAMESPACE_SEPARATOR;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns cache storage.
|
||||
*/
|
||||
final public function getStorage(): IStorage
|
||||
{
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns cache namespace.
|
||||
*/
|
||||
final public function getNamespace(): string
|
||||
{
|
||||
return (string) substr($this->namespace, 0, -1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns new nested cache object.
|
||||
* @return static
|
||||
*/
|
||||
public function derive(string $namespace)
|
||||
{
|
||||
$derived = new static($this->storage, $this->namespace . $namespace);
|
||||
return $derived;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the specified item from the cache or generate it.
|
||||
* @param mixed $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function load($key, callable $fallback = null)
|
||||
{
|
||||
$data = $this->storage->read($this->generateKey($key));
|
||||
if ($data === null && $fallback) {
|
||||
return $this->save($key, function (&$dependencies) use ($fallback) {
|
||||
return $fallback(...[&$dependencies]);
|
||||
});
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads multiple items from the cache.
|
||||
*/
|
||||
public function bulkLoad(array $keys, callable $fallback = null): array
|
||||
{
|
||||
if (count($keys) === 0) {
|
||||
return [];
|
||||
}
|
||||
foreach ($keys as $key) {
|
||||
if (!is_scalar($key)) {
|
||||
throw new Nette\InvalidArgumentException('Only scalar keys are allowed in bulkLoad()');
|
||||
}
|
||||
}
|
||||
$storageKeys = array_map([$this, 'generateKey'], $keys);
|
||||
if (!$this->storage instanceof IBulkReader) {
|
||||
$result = array_combine($keys, array_map([$this->storage, 'read'], $storageKeys));
|
||||
if ($fallback !== null) {
|
||||
foreach ($result as $key => $value) {
|
||||
if ($value === null) {
|
||||
$result[$key] = $this->save($key, function (&$dependencies) use ($key, $fallback) {
|
||||
return $fallback(...[$key, &$dependencies]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
$cacheData = $this->storage->bulkRead($storageKeys);
|
||||
$result = [];
|
||||
foreach ($keys as $i => $key) {
|
||||
$storageKey = $storageKeys[$i];
|
||||
if (isset($cacheData[$storageKey])) {
|
||||
$result[$key] = $cacheData[$storageKey];
|
||||
} elseif ($fallback) {
|
||||
$result[$key] = $this->save($key, function (&$dependencies) use ($key, $fallback) {
|
||||
return $fallback(...[$key, &$dependencies]);
|
||||
});
|
||||
} else {
|
||||
$result[$key] = null;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes item into the cache.
|
||||
* Dependencies are:
|
||||
* - Cache::PRIORITY => (int) priority
|
||||
* - Cache::EXPIRATION => (timestamp) expiration
|
||||
* - Cache::SLIDING => (bool) use sliding expiration?
|
||||
* - Cache::TAGS => (array) tags
|
||||
* - Cache::FILES => (array|string) file names
|
||||
* - Cache::ITEMS => (array|string) cache items
|
||||
* - Cache::CONSTS => (array|string) cache items
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param mixed $data
|
||||
* @return mixed value itself
|
||||
* @throws Nette\InvalidArgumentException
|
||||
*/
|
||||
public function save($key, $data, array $dependencies = null)
|
||||
{
|
||||
$key = $this->generateKey($key);
|
||||
|
||||
if ($data instanceof \Closure) {
|
||||
$this->storage->lock($key);
|
||||
try {
|
||||
$data = $data(...[&$dependencies]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->storage->remove($key);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($data === null) {
|
||||
$this->storage->remove($key);
|
||||
} else {
|
||||
$dependencies = $this->completeDependencies($dependencies);
|
||||
if (isset($dependencies[self::EXPIRATION]) && $dependencies[self::EXPIRATION] <= 0) {
|
||||
$this->storage->remove($key);
|
||||
} else {
|
||||
$this->storage->write($key, $data, $dependencies);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function completeDependencies(?array $dp): array
|
||||
{
|
||||
// convert expire into relative amount of seconds
|
||||
if (isset($dp[self::EXPIRATION])) {
|
||||
$dp[self::EXPIRATION] = Nette\Utils\DateTime::from($dp[self::EXPIRATION])->format('U') - time();
|
||||
}
|
||||
|
||||
// make list from TAGS
|
||||
if (isset($dp[self::TAGS])) {
|
||||
$dp[self::TAGS] = array_values((array) $dp[self::TAGS]);
|
||||
}
|
||||
|
||||
// make list from NAMESPACES
|
||||
if (isset($dp[self::NAMESPACES])) {
|
||||
$dp[self::NAMESPACES] = array_values((array) $dp[self::NAMESPACES]);
|
||||
}
|
||||
|
||||
// convert FILES into CALLBACKS
|
||||
if (isset($dp[self::FILES])) {
|
||||
foreach (array_unique((array) $dp[self::FILES]) as $item) {
|
||||
$dp[self::CALLBACKS][] = [[__CLASS__, 'checkFile'], $item, @filemtime($item) ?: null]; // @ - stat may fail
|
||||
}
|
||||
unset($dp[self::FILES]);
|
||||
}
|
||||
|
||||
// add namespaces to items
|
||||
if (isset($dp[self::ITEMS])) {
|
||||
$dp[self::ITEMS] = array_unique(array_map([$this, 'generateKey'], (array) $dp[self::ITEMS]));
|
||||
}
|
||||
|
||||
// convert CONSTS into CALLBACKS
|
||||
if (isset($dp[self::CONSTS])) {
|
||||
foreach (array_unique((array) $dp[self::CONSTS]) as $item) {
|
||||
$dp[self::CALLBACKS][] = [[__CLASS__, 'checkConst'], $item, constant($item)];
|
||||
}
|
||||
unset($dp[self::CONSTS]);
|
||||
}
|
||||
|
||||
if (!is_array($dp)) {
|
||||
$dp = [];
|
||||
}
|
||||
return $dp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes item from the cache.
|
||||
* @param mixed $key
|
||||
*/
|
||||
public function remove($key): void
|
||||
{
|
||||
$this->save($key, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes items from the cache by conditions.
|
||||
* Conditions are:
|
||||
* - Cache::PRIORITY => (int) priority
|
||||
* - Cache::TAGS => (array) tags
|
||||
* - Cache::ALL => true
|
||||
*/
|
||||
public function clean(array $conditions = null): void
|
||||
{
|
||||
$conditions = (array) $conditions;
|
||||
if (isset($conditions[self::TAGS])) {
|
||||
$conditions[self::TAGS] = array_values((array) $conditions[self::TAGS]);
|
||||
}
|
||||
$this->storage->clean($conditions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Caches results of function/method calls.
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(callable $function)
|
||||
{
|
||||
$key = func_get_args();
|
||||
if (is_array($function) && is_object($function[0])) {
|
||||
$key[0][0] = get_class($function[0]);
|
||||
}
|
||||
return $this->load($key, function () use ($function, $key) {
|
||||
return $function(...array_slice($key, 1));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Caches results of function/method calls.
|
||||
*/
|
||||
public function wrap(callable $function, array $dependencies = null): \Closure
|
||||
{
|
||||
return function () use ($function, $dependencies) {
|
||||
$key = [$function, func_get_args()];
|
||||
if (is_array($function) && is_object($function[0])) {
|
||||
$key[0][0] = get_class($function[0]);
|
||||
}
|
||||
$data = $this->load($key);
|
||||
if ($data === null) {
|
||||
$data = $this->save($key, $function(...$key[1]), $dependencies);
|
||||
}
|
||||
return $data;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the output cache.
|
||||
* @param mixed $key
|
||||
*/
|
||||
public function start($key): ?OutputHelper
|
||||
{
|
||||
$data = $this->load($key);
|
||||
if ($data === null) {
|
||||
return new OutputHelper($this, $key);
|
||||
}
|
||||
echo $data;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates internal cache key.
|
||||
*/
|
||||
protected function generateKey($key): string
|
||||
{
|
||||
return $this->namespace . md5(is_scalar($key) ? (string) $key : serialize($key));
|
||||
}
|
||||
|
||||
|
||||
/********************* dependency checkers ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Checks CALLBACKS dependencies.
|
||||
*/
|
||||
public static function checkCallbacks(array $callbacks): bool
|
||||
{
|
||||
foreach ($callbacks as $callback) {
|
||||
if (!array_shift($callback)(...$callback)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks CONSTS dependency.
|
||||
*/
|
||||
private static function checkConst(string $const, $value): bool
|
||||
{
|
||||
return defined($const) && constant($const) === $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks FILES dependency.
|
||||
*/
|
||||
private static function checkFile(string $file, ?int $time): bool
|
||||
{
|
||||
return @filemtime($file) == $time; // @ - stat may fail
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching;
|
||||
|
||||
|
||||
/**
|
||||
* Cache storage with a bulk read support.
|
||||
*/
|
||||
interface IBulkReader
|
||||
{
|
||||
|
||||
/**
|
||||
* Reads from cache in bulk.
|
||||
* @return array key => value pairs, missing items are omitted
|
||||
*/
|
||||
function bulkRead(array $keys): array;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching;
|
||||
|
||||
|
||||
/**
|
||||
* Cache storage.
|
||||
*/
|
||||
interface IStorage
|
||||
{
|
||||
|
||||
/**
|
||||
* Read from cache.
|
||||
* @return mixed
|
||||
*/
|
||||
function read(string $key);
|
||||
|
||||
/**
|
||||
* Prevents item reading and writing. Lock is released by write() or remove().
|
||||
*/
|
||||
function lock(string $key): void;
|
||||
|
||||
/**
|
||||
* Writes item into the cache.
|
||||
*/
|
||||
function write(string $key, $data, array $dependencies): void;
|
||||
|
||||
/**
|
||||
* Removes item from the cache.
|
||||
*/
|
||||
function remove(string $key): void;
|
||||
|
||||
/**
|
||||
* Removes items from the cache by conditions.
|
||||
*/
|
||||
function clean(array $conditions): void;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Output caching helper.
|
||||
*/
|
||||
class OutputHelper
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array */
|
||||
public $dependencies = [];
|
||||
|
||||
/** @var Cache|null */
|
||||
private $cache;
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
|
||||
public function __construct(Cache $cache, $key)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->key = $key;
|
||||
ob_start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stops and saves the cache.
|
||||
*/
|
||||
public function end(array $dependencies = []): void
|
||||
{
|
||||
if ($this->cache === null) {
|
||||
throw new Nette\InvalidStateException('Output cache has already been saved.');
|
||||
}
|
||||
$this->cache->save($this->key, ob_get_flush(), $dependencies + $this->dependencies);
|
||||
$this->cache = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Cache dummy storage.
|
||||
*/
|
||||
class DevNullStorage implements Nette\Caching\IStorage
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
public function read(string $key)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function lock(string $key): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, $data, array $dependencies): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
use Nette\Caching\Cache;
|
||||
|
||||
|
||||
/**
|
||||
* Cache file storage.
|
||||
*/
|
||||
class FileStorage implements Nette\Caching\IStorage
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/**
|
||||
* Atomic thread safe logic:
|
||||
*
|
||||
* 1) reading: open(r+b), lock(SH), read
|
||||
* - delete?: delete*, close
|
||||
* 2) deleting: delete*
|
||||
* 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
|
||||
*
|
||||
* delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
|
||||
*/
|
||||
|
||||
/** @internal cache file structure: meta-struct size + serialized meta-struct + data */
|
||||
private const
|
||||
META_HEADER_LEN = 6,
|
||||
// meta structure: array of
|
||||
META_TIME = 'time', // timestamp
|
||||
META_SERIALIZED = 'serialized', // is content serialized?
|
||||
META_EXPIRE = 'expire', // expiration timestamp
|
||||
META_DELTA = 'delta', // relative (sliding) expiration
|
||||
META_ITEMS = 'di', // array of dependent items (file => timestamp)
|
||||
META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
|
||||
|
||||
/** additional cache structure */
|
||||
private const
|
||||
FILE = 'file',
|
||||
HANDLE = 'handle';
|
||||
|
||||
/** @var float probability that the clean() routine is started */
|
||||
public static $gcProbability = 0.001;
|
||||
|
||||
/** @deprecated */
|
||||
public static $useDirectories = true;
|
||||
|
||||
/** @var string */
|
||||
private $dir;
|
||||
|
||||
/** @var IJournal */
|
||||
private $journal;
|
||||
|
||||
/** @var array */
|
||||
private $locks;
|
||||
|
||||
|
||||
public function __construct(string $dir, IJournal $journal = null)
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
|
||||
}
|
||||
|
||||
$this->dir = $dir;
|
||||
$this->journal = $journal;
|
||||
|
||||
if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
|
||||
$this->clean([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function read(string $key)
|
||||
{
|
||||
$meta = $this->readMetaAndLock($this->getCacheFile($key), LOCK_SH);
|
||||
if ($meta && $this->verify($meta)) {
|
||||
return $this->readData($meta); // calls fclose()
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verifies dependencies.
|
||||
*/
|
||||
private function verify(array $meta): bool
|
||||
{
|
||||
do {
|
||||
if (!empty($meta[self::META_DELTA])) {
|
||||
// meta[file] was added by readMetaAndLock()
|
||||
if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) {
|
||||
break;
|
||||
}
|
||||
touch($meta[self::FILE]);
|
||||
|
||||
} elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($meta[self::META_ITEMS])) {
|
||||
foreach ($meta[self::META_ITEMS] as $depFile => $time) {
|
||||
$m = $this->readMetaAndLock($depFile, LOCK_SH);
|
||||
if (($m[self::META_TIME] ?? null) !== $time || ($m && !$this->verify($m))) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} while (false);
|
||||
|
||||
$this->delete($meta[self::FILE], $meta[self::HANDLE]); // meta[handle] & meta[file] was added by readMetaAndLock()
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function lock(string $key): void
|
||||
{
|
||||
$cacheFile = $this->getCacheFile($key);
|
||||
if (!is_dir($dir = dirname($cacheFile))) {
|
||||
@mkdir($dir); // @ - directory may already exist
|
||||
}
|
||||
$handle = fopen($cacheFile, 'c+b');
|
||||
if ($handle) {
|
||||
$this->locks[$key] = $handle;
|
||||
flock($handle, LOCK_EX);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, $data, array $dp): void
|
||||
{
|
||||
$meta = [
|
||||
self::META_TIME => microtime(),
|
||||
];
|
||||
|
||||
if (isset($dp[Cache::EXPIRATION])) {
|
||||
if (empty($dp[Cache::SLIDING])) {
|
||||
$meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); // absolute time
|
||||
} else {
|
||||
$meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; // sliding time
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($dp[Cache::ITEMS])) {
|
||||
foreach ($dp[Cache::ITEMS] as $item) {
|
||||
$depFile = $this->getCacheFile($item);
|
||||
$m = $this->readMetaAndLock($depFile, LOCK_SH);
|
||||
$meta[self::META_ITEMS][$depFile] = $m[self::META_TIME] ?? null;
|
||||
unset($m);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($dp[Cache::CALLBACKS])) {
|
||||
$meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
|
||||
}
|
||||
|
||||
if (!isset($this->locks[$key])) {
|
||||
$this->lock($key);
|
||||
if (!isset($this->locks[$key])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$handle = $this->locks[$key];
|
||||
unset($this->locks[$key]);
|
||||
|
||||
$cacheFile = $this->getCacheFile($key);
|
||||
|
||||
if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
|
||||
if (!$this->journal) {
|
||||
throw new Nette\InvalidStateException('CacheJournal has not been provided.');
|
||||
}
|
||||
$this->journal->write($cacheFile, $dp);
|
||||
}
|
||||
|
||||
ftruncate($handle, 0);
|
||||
|
||||
if (!is_string($data)) {
|
||||
$data = serialize($data);
|
||||
$meta[self::META_SERIALIZED] = true;
|
||||
}
|
||||
|
||||
$head = serialize($meta);
|
||||
$head = str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
|
||||
$headLen = strlen($head);
|
||||
|
||||
do {
|
||||
if (fwrite($handle, str_repeat("\x00", $headLen)) !== $headLen) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (fwrite($handle, $data) !== strlen($data)) {
|
||||
break;
|
||||
}
|
||||
|
||||
fseek($handle, 0);
|
||||
if (fwrite($handle, $head) !== $headLen) {
|
||||
break;
|
||||
}
|
||||
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
return;
|
||||
} while (false);
|
||||
|
||||
$this->delete($cacheFile, $handle);
|
||||
}
|
||||
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
unset($this->locks[$key]);
|
||||
$this->delete($this->getCacheFile($key));
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): void
|
||||
{
|
||||
$all = !empty($conditions[Cache::ALL]);
|
||||
$collector = empty($conditions);
|
||||
$namespaces = $conditions[Cache::NAMESPACES] ?? null;
|
||||
|
||||
// cleaning using file iterator
|
||||
if ($all || $collector) {
|
||||
$now = time();
|
||||
foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
|
||||
$path = (string) $entry;
|
||||
if ($entry->isDir()) { // collector: remove empty dirs
|
||||
@rmdir($path); // @ - removing dirs is not necessary
|
||||
continue;
|
||||
}
|
||||
if ($all) {
|
||||
$this->delete($path);
|
||||
|
||||
} else { // collector
|
||||
$meta = $this->readMetaAndLock($path, LOCK_SH);
|
||||
if (!$meta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
|
||||
|| (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
|
||||
) {
|
||||
$this->delete($path, $meta[self::HANDLE]);
|
||||
continue;
|
||||
}
|
||||
|
||||
flock($meta[self::HANDLE], LOCK_UN);
|
||||
fclose($meta[self::HANDLE]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->journal) {
|
||||
$this->journal->clean($conditions);
|
||||
}
|
||||
return;
|
||||
|
||||
} elseif ($namespaces) {
|
||||
foreach ($namespaces as $namespace) {
|
||||
$dir = $this->dir . '/_' . urlencode($namespace);
|
||||
if (is_dir($dir)) {
|
||||
foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
|
||||
$this->delete((string) $entry);
|
||||
}
|
||||
@rmdir($dir); // may already contain new files
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleaning using journal
|
||||
if ($this->journal) {
|
||||
foreach ($this->journal->clean($conditions) as $file) {
|
||||
$this->delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads cache data from disk.
|
||||
*/
|
||||
protected function readMetaAndLock(string $file, int $lock): ?array
|
||||
{
|
||||
$handle = @fopen($file, 'r+b'); // @ - file may not exist
|
||||
if (!$handle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
flock($handle, $lock);
|
||||
|
||||
$size = (int) stream_get_contents($handle, self::META_HEADER_LEN);
|
||||
if ($size) {
|
||||
$meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
|
||||
$meta = unserialize($meta);
|
||||
$meta[self::FILE] = $file;
|
||||
$meta[self::HANDLE] = $handle;
|
||||
return $meta;
|
||||
}
|
||||
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads cache data from disk and closes cache file handle.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function readData(array $meta)
|
||||
{
|
||||
$data = stream_get_contents($meta[self::HANDLE]);
|
||||
flock($meta[self::HANDLE], LOCK_UN);
|
||||
fclose($meta[self::HANDLE]);
|
||||
|
||||
if (empty($meta[self::META_SERIALIZED])) {
|
||||
return $data;
|
||||
} else {
|
||||
return unserialize($data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns file name.
|
||||
*/
|
||||
protected function getCacheFile(string $key): string
|
||||
{
|
||||
$file = urlencode($key);
|
||||
if ($a = strrpos($file, '%00')) { // %00 = urlencode(Nette\Caching\Cache::NAMESPACE_SEPARATOR)
|
||||
$file = substr_replace($file, '/_', $a, 3);
|
||||
}
|
||||
return $this->dir . '/_' . $file;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes and closes file.
|
||||
* @param resource $handle
|
||||
*/
|
||||
private static function delete(string $file, $handle = null): void
|
||||
{
|
||||
if (@unlink($file)) { // @ - file may not already exist
|
||||
if ($handle) {
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$handle) {
|
||||
$handle = @fopen($file, 'r+'); // @ - file may not exist
|
||||
}
|
||||
if ($handle) {
|
||||
flock($handle, LOCK_EX);
|
||||
ftruncate($handle, 0);
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
@unlink($file); // @ - file may not already exist
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
|
||||
/**
|
||||
* Cache journal provider.
|
||||
*/
|
||||
interface IJournal
|
||||
{
|
||||
|
||||
/**
|
||||
* Writes entry information into the journal.
|
||||
*/
|
||||
function write(string $key, array $dependencies): void;
|
||||
|
||||
/**
|
||||
* Cleans entries from journal.
|
||||
* @return array|null of removed items or null when performing a full cleanup
|
||||
*/
|
||||
function clean(array $conditions): ?array;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
use Nette\Caching\Cache;
|
||||
|
||||
|
||||
/**
|
||||
* Memcached storage using memcached extension.
|
||||
*/
|
||||
class MemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @internal cache structure */
|
||||
private const
|
||||
META_CALLBACKS = 'callbacks',
|
||||
META_DATA = 'data',
|
||||
META_DELTA = 'delta';
|
||||
|
||||
/** @var \Memcached */
|
||||
private $memcached;
|
||||
|
||||
/** @var string */
|
||||
private $prefix;
|
||||
|
||||
/** @var IJournal */
|
||||
private $journal;
|
||||
|
||||
|
||||
/**
|
||||
* Checks if Memcached extension is available.
|
||||
*/
|
||||
public static function isAvailable(): bool
|
||||
{
|
||||
return extension_loaded('memcached');
|
||||
}
|
||||
|
||||
|
||||
public function __construct(string $host = 'localhost', int $port = 11211, string $prefix = '', IJournal $journal = null)
|
||||
{
|
||||
if (!static::isAvailable()) {
|
||||
throw new Nette\NotSupportedException("PHP extension 'memcached' is not loaded.");
|
||||
}
|
||||
|
||||
$this->prefix = $prefix;
|
||||
$this->journal = $journal;
|
||||
$this->memcached = new \Memcached;
|
||||
if ($host) {
|
||||
$this->addServer($host, $port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function addServer(string $host = 'localhost', int $port = 11211): void
|
||||
{
|
||||
if ($this->memcached->addServer($host, $port, 1) === false) {
|
||||
$error = error_get_last();
|
||||
throw new Nette\InvalidStateException("Memcached::addServer(): $error[message].");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getConnection(): \Memcached
|
||||
{
|
||||
return $this->memcached;
|
||||
}
|
||||
|
||||
|
||||
public function read(string $key)
|
||||
{
|
||||
$key = urlencode($this->prefix . $key);
|
||||
$meta = $this->memcached->get($key);
|
||||
if (!$meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// meta structure:
|
||||
// array(
|
||||
// data => stored data
|
||||
// delta => relative (sliding) expiration
|
||||
// callbacks => array of callbacks (function, args)
|
||||
// )
|
||||
|
||||
// verify dependencies
|
||||
if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
|
||||
$this->memcached->delete($key, 0);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($meta[self::META_DELTA])) {
|
||||
$this->memcached->replace($key, $meta, $meta[self::META_DELTA] + time());
|
||||
}
|
||||
|
||||
return $meta[self::META_DATA];
|
||||
}
|
||||
|
||||
|
||||
public function bulkRead(array $keys): array
|
||||
{
|
||||
$prefixedKeys = array_map(function ($key) {
|
||||
return urlencode($this->prefix . $key);
|
||||
}, $keys);
|
||||
$keys = array_combine($prefixedKeys, $keys);
|
||||
$metas = $this->memcached->getMulti($prefixedKeys);
|
||||
$result = [];
|
||||
$deleteKeys = [];
|
||||
foreach ($metas as $prefixedKey => $meta) {
|
||||
if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
|
||||
$deleteKeys[] = $prefixedKey;
|
||||
} else {
|
||||
$result[$keys[$prefixedKey]] = $meta[self::META_DATA];
|
||||
}
|
||||
|
||||
if (!empty($meta[self::META_DELTA])) {
|
||||
$this->memcached->replace($prefixedKey, $meta, $meta[self::META_DELTA] + time());
|
||||
}
|
||||
}
|
||||
if (!empty($deleteKeys)) {
|
||||
$this->memcached->deleteMulti($deleteKeys, 0);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function lock(string $key): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, $data, array $dp): void
|
||||
{
|
||||
if (isset($dp[Cache::ITEMS])) {
|
||||
throw new Nette\NotSupportedException('Dependent items are not supported by MemcachedStorage.');
|
||||
}
|
||||
|
||||
$key = urlencode($this->prefix . $key);
|
||||
$meta = [
|
||||
self::META_DATA => $data,
|
||||
];
|
||||
|
||||
$expire = 0;
|
||||
if (isset($dp[Cache::EXPIRATION])) {
|
||||
$expire = (int) $dp[Cache::EXPIRATION];
|
||||
if (!empty($dp[Cache::SLIDING])) {
|
||||
$meta[self::META_DELTA] = $expire; // sliding time
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($dp[Cache::CALLBACKS])) {
|
||||
$meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
|
||||
}
|
||||
|
||||
if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
|
||||
if (!$this->journal) {
|
||||
throw new Nette\InvalidStateException('CacheJournal has not been provided.');
|
||||
}
|
||||
$this->journal->write($key, $dp);
|
||||
}
|
||||
|
||||
$this->memcached->set($key, $meta, $expire);
|
||||
}
|
||||
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
$this->memcached->delete(urlencode($this->prefix . $key), 0);
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): void
|
||||
{
|
||||
if (!empty($conditions[Cache::ALL])) {
|
||||
$this->memcached->flush();
|
||||
|
||||
} elseif ($this->journal) {
|
||||
foreach ($this->journal->clean($conditions) as $entry) {
|
||||
$this->memcached->delete($entry, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Memory cache storage.
|
||||
*/
|
||||
class MemoryStorage implements Nette\Caching\IStorage
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array */
|
||||
private $data = [];
|
||||
|
||||
|
||||
public function read(string $key)
|
||||
{
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
|
||||
|
||||
public function lock(string $key): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, $data, array $dependencies): void
|
||||
{
|
||||
$this->data[$key] = $data;
|
||||
}
|
||||
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): void
|
||||
{
|
||||
if (!empty($conditions[Nette\Caching\Cache::ALL])) {
|
||||
$this->data = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class NewMemcachedStorage extends MemcachedStorage
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
use Nette\Caching\Cache;
|
||||
|
||||
|
||||
/**
|
||||
* SQLite based journal.
|
||||
*/
|
||||
class SQLiteJournal implements IJournal
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @string */
|
||||
private $path;
|
||||
|
||||
/** @var \PDO */
|
||||
private $pdo;
|
||||
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
if (!extension_loaded('pdo_sqlite')) {
|
||||
throw new Nette\NotSupportedException('SQLiteJournal requires PHP extension pdo_sqlite which is not loaded.');
|
||||
}
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
|
||||
private function open(): void
|
||||
{
|
||||
if ($this->path !== ':memory:' && !is_file($this->path)) {
|
||||
touch($this->path); // ensures ordinary file permissions
|
||||
}
|
||||
|
||||
$this->pdo = new \PDO('sqlite:' . $this->path);
|
||||
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->exec('
|
||||
PRAGMA foreign_keys = OFF;
|
||||
PRAGMA journal_mode = WAL;
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
key BLOB NOT NULL,
|
||||
tag BLOB NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS priorities (
|
||||
key BLOB NOT NULL,
|
||||
priority INT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_key_tag ON tags(key, tag);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_priorities_key ON priorities(key);
|
||||
CREATE INDEX IF NOT EXISTS idx_priorities_priority ON priorities(priority);
|
||||
');
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, array $dependencies): void
|
||||
{
|
||||
if (!$this->pdo) {
|
||||
$this->open();
|
||||
}
|
||||
$this->pdo->exec('BEGIN');
|
||||
|
||||
if (!empty($dependencies[Cache::TAGS])) {
|
||||
$this->pdo->prepare('DELETE FROM tags WHERE key = ?')->execute([$key]);
|
||||
|
||||
foreach ($dependencies[Cache::TAGS] as $tag) {
|
||||
$arr[] = $key;
|
||||
$arr[] = $tag;
|
||||
}
|
||||
$this->pdo->prepare('INSERT INTO tags (key, tag) SELECT ?, ?' . str_repeat('UNION SELECT ?, ?', count($arr) / 2 - 1))
|
||||
->execute($arr);
|
||||
}
|
||||
|
||||
if (!empty($dependencies[Cache::PRIORITY])) {
|
||||
$this->pdo->prepare('REPLACE INTO priorities (key, priority) VALUES (?, ?)')
|
||||
->execute([$key, (int) $dependencies[Cache::PRIORITY]]);
|
||||
}
|
||||
|
||||
$this->pdo->exec('COMMIT');
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): ?array
|
||||
{
|
||||
if (!$this->pdo) {
|
||||
$this->open();
|
||||
}
|
||||
if (!empty($conditions[Cache::ALL])) {
|
||||
$this->pdo->exec('
|
||||
BEGIN;
|
||||
DELETE FROM tags;
|
||||
DELETE FROM priorities;
|
||||
COMMIT;
|
||||
');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$unions = $args = [];
|
||||
if (!empty($conditions[Cache::TAGS])) {
|
||||
$tags = (array) $conditions[Cache::TAGS];
|
||||
$unions[] = 'SELECT DISTINCT key FROM tags WHERE tag IN (?' . str_repeat(', ?', count($tags) - 1) . ')';
|
||||
$args = $tags;
|
||||
}
|
||||
|
||||
if (!empty($conditions[Cache::PRIORITY])) {
|
||||
$unions[] = 'SELECT DISTINCT key FROM priorities WHERE priority <= ?';
|
||||
$args[] = (int) $conditions[Cache::PRIORITY];
|
||||
}
|
||||
|
||||
if (empty($unions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$unionSql = implode(' UNION ', $unions);
|
||||
|
||||
$this->pdo->exec('BEGIN IMMEDIATE');
|
||||
|
||||
$stmt = $this->pdo->prepare($unionSql);
|
||||
$stmt->execute($args);
|
||||
$keys = $stmt->fetchAll(\PDO::FETCH_COLUMN, 0);
|
||||
|
||||
if (empty($keys)) {
|
||||
$this->pdo->exec('COMMIT');
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->pdo->prepare("DELETE FROM tags WHERE key IN ($unionSql)")->execute($args);
|
||||
$this->pdo->prepare("DELETE FROM priorities WHERE key IN ($unionSql)")->execute($args);
|
||||
$this->pdo->exec('COMMIT');
|
||||
|
||||
return $keys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Caching\Storages;
|
||||
|
||||
use Nette;
|
||||
use Nette\Caching\Cache;
|
||||
|
||||
|
||||
/**
|
||||
* SQLite storage.
|
||||
*/
|
||||
class SQLiteStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var \PDO */
|
||||
private $pdo;
|
||||
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
if ($path !== ':memory:' && !is_file($path)) {
|
||||
touch($path); // ensures ordinary file permissions
|
||||
}
|
||||
|
||||
$this->pdo = new \PDO('sqlite:' . $path);
|
||||
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->exec('
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key BLOB NOT NULL PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
expire INTEGER,
|
||||
slide INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
key BLOB NOT NULL REFERENCES cache ON DELETE CASCADE,
|
||||
tag BLOB NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS cache_expire ON cache(expire);
|
||||
CREATE INDEX IF NOT EXISTS tags_key ON tags(key);
|
||||
CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag);
|
||||
PRAGMA synchronous = OFF;
|
||||
');
|
||||
}
|
||||
|
||||
|
||||
public function read(string $key)
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT data, slide FROM cache WHERE key=? AND (expire IS NULL OR expire >= ?)');
|
||||
$stmt->execute([$key, time()]);
|
||||
if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
if ($row['slide'] !== null) {
|
||||
$this->pdo->prepare('UPDATE cache SET expire = ? + slide WHERE key=?')->execute([time(), $key]);
|
||||
}
|
||||
return unserialize($row['data']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function bulkRead(array $keys): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT key, data, slide FROM cache WHERE key IN (?' . str_repeat(',?', count($keys) - 1) . ') AND (expire IS NULL OR expire >= ?)');
|
||||
$stmt->execute(array_merge($keys, [time()]));
|
||||
$result = [];
|
||||
$updateSlide = [];
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
|
||||
if ($row['slide'] !== null) {
|
||||
$updateSlide[] = $row['key'];
|
||||
}
|
||||
$result[$row['key']] = unserialize($row['data']);
|
||||
}
|
||||
if (!empty($updateSlide)) {
|
||||
$stmt = $this->pdo->prepare('UPDATE cache SET expire = ? + slide WHERE key IN(?' . str_repeat(',?', count($updateSlide) - 1) . ')');
|
||||
$stmt->execute(array_merge([time()], $updateSlide));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function lock(string $key): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function write(string $key, $data, array $dependencies): void
|
||||
{
|
||||
$expire = isset($dependencies[Cache::EXPIRATION]) ? $dependencies[Cache::EXPIRATION] + time() : null;
|
||||
$slide = isset($dependencies[Cache::SLIDING]) ? $dependencies[Cache::EXPIRATION] : null;
|
||||
|
||||
$this->pdo->exec('BEGIN TRANSACTION');
|
||||
$this->pdo->prepare('REPLACE INTO cache (key, data, expire, slide) VALUES (?, ?, ?, ?)')
|
||||
->execute([$key, serialize($data), $expire, $slide]);
|
||||
|
||||
if (!empty($dependencies[Cache::TAGS])) {
|
||||
foreach ($dependencies[Cache::TAGS] as $tag) {
|
||||
$arr[] = $key;
|
||||
$arr[] = $tag;
|
||||
}
|
||||
$this->pdo->prepare('INSERT INTO tags (key, tag) SELECT ?, ?' . str_repeat('UNION SELECT ?, ?', count($arr) / 2 - 1))
|
||||
->execute($arr);
|
||||
}
|
||||
$this->pdo->exec('COMMIT');
|
||||
}
|
||||
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM cache WHERE key=?')
|
||||
->execute([$key]);
|
||||
}
|
||||
|
||||
|
||||
public function clean(array $conditions): void
|
||||
{
|
||||
if (!empty($conditions[Cache::ALL])) {
|
||||
$this->pdo->prepare('DELETE FROM cache')->execute();
|
||||
|
||||
} else {
|
||||
$sql = 'DELETE FROM cache WHERE expire < ?';
|
||||
$args = [time()];
|
||||
|
||||
if (!empty($conditions[Cache::TAGS])) {
|
||||
$tags = $conditions[Cache::TAGS];
|
||||
$sql .= ' OR key IN (SELECT key FROM tags WHERE tag IN (?' . str_repeat(',?', count($tags) - 1) . '))';
|
||||
$args = array_merge($args, $tags);
|
||||
}
|
||||
|
||||
$this->pdo->prepare($sql)->execute($args);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user