file_cache_준비

This commit is contained in:
2020-06-26 21:24:24 +09:00
parent 05904d31fd
commit 40968b301a
60 changed files with 8621 additions and 109 deletions
+353
View File
@@ -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
}
}
+24
View File
@@ -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
View File
@@ -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;
}
+51
View File
@@ -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
}
}
}
+29
View File
@@ -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);
}
}
}