no message

This commit is contained in:
BarboraFleg
2019-04-04 18:34:40 +02:00
commit ce48ca4a21
589 changed files with 82184 additions and 0 deletions
@@ -0,0 +1,77 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Bridges\CacheDI;
use Nette;
/**
* Cache extension for Nette DI.
*/
class CacheExtension extends Nette\DI\CompilerExtension
{
/** @var string */
private $tempDir;
public function __construct($tempDir)
{
$this->tempDir = $tempDir;
}
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
if (extension_loaded('pdo_sqlite')) {
$builder->addDefinition($this->prefix('journal'))
->setClass(Nette\Caching\Storages\IJournal::class)
->setFactory(Nette\Caching\Storages\SQLiteJournal::class, [$this->tempDir . '/cache/journal.s3db']);
}
$builder->addDefinition($this->prefix('storage'))
->setClass(Nette\Caching\IStorage::class)
->setFactory(Nette\Caching\Storages\FileStorage::class, [$this->tempDir . '/cache']);
if ($this->name === 'cache') {
if (extension_loaded('pdo_sqlite')) {
$builder->addAlias('nette.cacheJournal', $this->prefix('journal'));
}
$builder->addAlias('cacheStorage', $this->prefix('storage'));
}
}
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
if (!$this->checkTempDir($this->tempDir . '/cache')) {
$class->getMethod('initialize')->addBody('Nette\Caching\Storages\FileStorage::$useDirectories = false;');
}
}
private function checkTempDir($dir)
{
Nette\Utils\FileSystem::createDir($dir);
// checks whether directory is writable
$uniq = uniqid('_', true);
if (!@mkdir("$dir/$uniq")) { // @ - is escalated to exception
throw new Nette\InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
}
// checks whether subdirectory is writable
$isWritable = @file_put_contents("$dir/$uniq/_", '') !== false; // @ - error is expected
if ($isWritable) {
unlink("$dir/$uniq/_");
}
rmdir("$dir/$uniq");
return $isWritable;
}
}
@@ -0,0 +1,140 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Bridges\CacheLatte;
use Latte;
use Nette;
use Nette\Caching\Cache;
/**
* Macro {cache} ... {/cache}
*/
class CacheMacro implements Latte\IMacro
{
use Nette\SmartObject;
/** @var bool */
private $used;
/**
* Initializes before template parsing.
* @return void
*/
public function initialize()
{
$this->used = false;
}
/**
* Finishes template parsing.
* @return array(prolog, epilog)
*/
public function finalize()
{
if ($this->used) {
return ['Nette\Bridges\CacheLatte\CacheMacro::initRuntime($this);'];
}
}
/**
* New node is found.
* @return bool
*/
public function nodeOpened(Latte\MacroNode $node)
{
if ($node->modifiers) {
throw new Latte\CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
$this->used = true;
$node->empty = false;
$node->openingCode = Latte\PhpWriter::using($node)
->write('<?php if (Nette\Bridges\CacheLatte\CacheMacro::createCache($this->global->cacheStorage, %var, $this->global->cacheStack, %node.array?)) { ?>',
Nette\Utils\Random::generate()
);
}
/**
* Node is closed.
* @return void
*/
public function nodeClosed(Latte\MacroNode $node)
{
$node->closingCode = Latte\PhpWriter::using($node)
->write('<?php Nette\Bridges\CacheLatte\CacheMacro::endCache($this->global->cacheStack, %node.array?); } ?>');
}
/********************* run-time helpers ****************d*g**/
/**
* @return void
*/
public static function initRuntime(Latte\Runtime\Template $template)
{
if (!empty($template->global->cacheStack)) {
$file = (new \ReflectionClass($template))->getFileName();
if (@is_file($file)) { // @ - may trigger error
end($template->global->cacheStack)->dependencies[Cache::FILES][] = $file;
}
}
}
/**
* Starts the output cache. Returns Nette\Caching\OutputHelper object if buffering was started.
* @param string $key
* @param Nette\Caching\OutputHelper[] $parents
* @return Nette\Caching\OutputHelper|\stdClass
*/
public static function createCache(Nette\Caching\IStorage $cacheStorage, $key, &$parents, array $args = null)
{
if ($args) {
if (array_key_exists('if', $args) && !$args['if']) {
return $parents[] = new \stdClass;
}
$key = array_merge([$key], array_intersect_key($args, range(0, count($args))));
}
if ($parents) {
end($parents)->dependencies[Cache::ITEMS][] = $key;
}
$cache = new Cache($cacheStorage, 'Nette.Templating.Cache');
if ($helper = $cache->start($key)) {
$parents[] = $helper;
}
return $helper;
}
/**
* Ends the output cache.
* @param Nette\Caching\OutputHelper[] $parents
* @return void
*/
public static function endCache(&$parents, array $args = null)
{
$helper = array_pop($parents);
if ($helper instanceof Nette\Caching\OutputHelper) {
if (isset($args['dependencies'])) {
$args += call_user_func($args['dependencies']);
}
if (isset($args['expire'])) {
$args['expiration'] = $args['expire']; // back compatibility
}
$helper->dependencies[Cache::TAGS] = isset($args['tags']) ? $args['tags'] : null;
$helper->dependencies[Cache::EXPIRATION] = isset($args['expiration']) ? $args['expiration'] : '+ 7 days';
$helper->end();
}
}
}
+376
View File
@@ -0,0 +1,376 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Caching;
use Nette;
/**
* Implements the cache for a application.
*/
class Cache
{
use Nette\SmartObject;
/** dependency */
const
PRIORITY = 'priority',
EXPIRATION = 'expire',
EXPIRE = 'expire',
SLIDING = 'sliding',
TAGS = 'tags',
FILES = 'files',
ITEMS = 'items',
CONSTS = 'consts',
CALLBACKS = 'callbacks',
NAMESPACES = 'namespaces',
ALL = 'all';
/** @internal */
const NAMESPACE_SEPARATOR = "\x00";
/** @var IStorage */
private $storage;
/** @var string */
private $namespace;
public function __construct(IStorage $storage, $namespace = null)
{
$this->storage = $storage;
$this->namespace = $namespace . self::NAMESPACE_SEPARATOR;
}
/**
* Returns cache storage.
* @return IStorage
*/
public function getStorage()
{
return $this->storage;
}
/**
* Returns cache namespace.
* @return string
*/
public function getNamespace()
{
return (string) substr($this->namespace, 0, -1);
}
/**
* Returns new nested cache object.
* @param string $namespace
* @return static
*/
public function derive($namespace)
{
$derived = new static($this->storage, $this->namespace . $namespace);
return $derived;
}
/**
* Reads the specified item from the cache or generate it.
* @param mixed $key
* @param callable $fallback
* @return mixed
*/
public function load($key, $fallback = null)
{
$data = $this->storage->read($this->generateKey($key));
if ($data === null && $fallback) {
return $this->save($key, function (&$dependencies) use ($fallback) {
return call_user_func_array($fallback, [&$dependencies]);
});
}
return $data;
}
/**
* Reads multiple items from the cache.
* @param callable $fallback
* @return array
*/
public function bulkLoad(array $keys, $fallback = null)
{
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 call_user_func_array($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 call_user_func_array($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
*/
public function save($key, $data, array $dependencies = null)
{
$key = $this->generateKey($key);
if ($data instanceof Nette\Callback || $data instanceof \Closure) {
if ($data instanceof Nette\Callback) {
trigger_error('Nette\Callback is deprecated, use closure or Nette\Utils\Callback::toClosure().', E_USER_DEPRECATED);
}
$this->storage->lock($key);
try {
$data = call_user_func_array($data, [&$dependencies]);
} catch (\Exception $e) {
$this->storage->remove($key);
throw $e;
} 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($dp)
{
// 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
* @return void
*/
public function remove($key)
{
$this->save($key, null);
}
/**
* Removes items from the cache by conditions.
* Conditions are:
* - Cache::PRIORITY => (int) priority
* - Cache::TAGS => (array) tags
* - Cache::ALL => true
* @return void
*/
public function clean(array $conditions = null)
{
$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.
* @param callable $function
* @return mixed
*/
public function call($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 call_user_func_array($function, array_slice($key, 1));
});
}
/**
* Caches results of function/method calls.
* @param callable $function
* @return \Closure
*/
public function wrap($function, array $dependencies = null)
{
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, call_user_func_array($function, $key[1]), $dependencies);
}
return $data;
};
}
/**
* Starts the output cache.
* @param mixed $key
* @return OutputHelper|null
*/
public function start($key)
{
$data = $this->load($key);
if ($data === null) {
return new OutputHelper($this, $key);
}
echo $data;
}
/**
* Generates internal cache key.
* @param mixed $key
* @return string
*/
protected function generateKey($key)
{
return $this->namespace . md5(is_scalar($key) ? (string) $key : serialize($key));
}
/********************* dependency checkers ****************d*g**/
/**
* Checks CALLBACKS dependencies.
* @return bool
*/
public static function checkCallbacks(array $callbacks)
{
foreach ($callbacks as $callback) {
if (!call_user_func_array(array_shift($callback), $callback)) {
return false;
}
}
return true;
}
/**
* Checks CONSTS dependency.
* @param string $const
* @param mixed $value
* @return bool
*/
private static function checkConst($const, $value)
{
return defined($const) && constant($const) === $value;
}
/**
* Checks FILES dependency.
* @param string $file
* @param int|null $time
* @return bool
*/
private static function checkFile($file, $time)
{
return @filemtime($file) == $time; // @ - stat may fail
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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);
}
+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)
*/
namespace Nette\Caching;
/**
* Cache storage.
*/
interface IStorage
{
/**
* Read from cache.
* @param string $key
* @return mixed
*/
function read($key);
/**
* Prevents item reading and writing. Lock is released by write() or remove().
* @param string $key
* @return void
*/
function lock($key);
/**
* Writes item into the cache.
* @param string $key
* @param mixed $data
* @return void
*/
function write($key, $data, array $dependencies);
/**
* Removes item from the cache.
* @param string $key
* @return void
*/
function remove($key);
/**
* Removes items from the cache by conditions.
* @return void
*/
function clean(array $conditions);
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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.
* @return void
*/
public function end(array $dependencies = null)
{
if ($this->cache === null) {
throw new Nette\InvalidStateException('Output cache has already been saved.');
}
$this->cache->save($this->key, ob_get_flush(), (array) $dependencies + (array) $this->dependencies);
$this->cache = null;
}
}
@@ -0,0 +1,43 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Caching\Storages;
use Nette;
/**
* Cache dummy storage.
*/
class DevNullStorage implements Nette\Caching\IStorage
{
use Nette\SmartObject;
public function read($key)
{
}
public function lock($key)
{
}
public function write($key, $data, array $dependencies)
{
}
public function remove($key)
{
}
public function clean(array $conditions)
{
}
}
@@ -0,0 +1,386 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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 */
const
META_HEADER_LEN = 28, // 22b signature + 6b meta-struct size + serialized meta-struct + data
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 */
const FILE = 'file',
HANDLE = 'handle';
/** @var float probability that the clean() routine is started */
public static $gcProbability = 0.001;
/** @var bool */
public static $useDirectories = true;
/** @var string */
private $dir;
/** @var bool */
private $useDirs;
/** @var IJournal */
private $journal;
/** @var array */
private $locks;
public function __construct($dir, IJournal $journal = null)
{
if (!is_dir($dir)) {
throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
}
$this->dir = $dir;
$this->useDirs = (bool) static::$useDirectories;
$this->journal = $journal;
if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
$this->clean([]);
}
}
public function read($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.
* @return bool
*/
private function verify(array $meta)
{
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] !== $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($key)
{
$cacheFile = $this->getCacheFile($key);
if ($this->useDirs && !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($key, $data, array $dp)
{
$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 ((array) $dp[Cache::ITEMS] as $item) {
$depFile = $this->getCacheFile($item);
$m = $this->readMetaAndLock($depFile, LOCK_SH);
$meta[self::META_ITEMS][$depFile] = $m[self::META_TIME]; // may be 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 = '<?php //netteCache[01]' . 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($key)
{
unset($this->locks[$key]);
$this->delete($this->getCacheFile($key));
}
public function clean(array $conditions)
{
$all = !empty($conditions[Cache::ALL]);
$collector = empty($conditions);
$namespaces = isset($conditions[Cache::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.
* @param string $file
* @param int $lock
* @return array|null
*/
protected function readMetaAndLock($file, $lock)
{
$handle = @fopen($file, 'r+b'); // @ - file may not exist
if (!$handle) {
return null;
}
flock($handle, $lock);
$head = stream_get_contents($handle, self::META_HEADER_LEN);
if ($head && strlen($head) === self::META_HEADER_LEN) {
$size = (int) substr($head, -6);
$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.
* @param array $meta
* @return mixed
*/
protected function readData($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.
* @param string $key
* @return string
*/
protected function getCacheFile($key)
{
$file = urlencode($key);
if ($this->useDirs && $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 string $file
* @param resource $handle
* @return void
*/
private static function delete($file, $handle = null)
{
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)
*/
namespace Nette\Caching\Storages;
/**
* Cache journal provider.
*/
interface IJournal
{
/**
* Writes entry information into the journal.
* @param string $key
* @return void
*/
function write($key, array $dependencies);
/**
* Cleans entries from journal.
* @return array|null of removed items or null when performing a full cleanup
*/
function clean(array $conditions);
}
@@ -0,0 +1,165 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Caching\Storages;
use Nette;
use Nette\Caching\Cache;
/**
* Memcached storage.
*/
class MemcachedStorage implements Nette\Caching\IStorage
{
use Nette\SmartObject;
/** @internal cache structure */
const
META_CALLBACKS = 'callbacks',
META_DATA = 'data',
META_DELTA = 'delta';
/** @var \Memcache */
private $memcache;
/** @var string */
private $prefix;
/** @var IJournal */
private $journal;
/**
* Checks if Memcached extension is available.
* @return bool
*/
public static function isAvailable()
{
return extension_loaded('memcache');
}
public function __construct($host = 'localhost', $port = 11211, $prefix = '', IJournal $journal = null)
{
if (!static::isAvailable()) {
throw new Nette\NotSupportedException("PHP extension 'memcache' is not loaded.");
}
$this->prefix = $prefix;
$this->journal = $journal;
$this->memcache = new \Memcache;
if ($host) {
$this->addServer($host, $port);
}
}
public function addServer($host = 'localhost', $port = 11211, $timeout = 1)
{
if ($this->memcache->addServer($host, $port, true, 1, $timeout) === false) {
$error = error_get_last();
throw new Nette\InvalidStateException("Memcache::addServer(): $error[message].");
}
}
/**
* @return \Memcache
*/
public function getConnection()
{
return $this->memcache;
}
public function read($key)
{
$key = urlencode($this->prefix . $key);
$meta = $this->memcache->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->memcache->delete($key, 0);
return null;
}
if (!empty($meta[self::META_DELTA])) {
$this->memcache->replace($key, $meta, 0, $meta[self::META_DELTA] + time());
}
return $meta[self::META_DATA];
}
public function lock($key)
{
}
public function write($key, $data, array $dp)
{
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->memcache->set($key, $meta, 0, $expire);
}
public function remove($key)
{
$this->memcache->delete(urlencode($this->prefix . $key), 0);
}
public function clean(array $conditions)
{
if (!empty($conditions[Cache::ALL])) {
$this->memcache->flush();
} elseif ($this->journal) {
foreach ($this->journal->clean($conditions) as $entry) {
$this->memcache->delete($entry, 0);
}
}
}
}
@@ -0,0 +1,53 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
public function lock($key)
{
}
public function write($key, $data, array $dependencies)
{
$this->data[$key] = $data;
}
public function remove($key)
{
unset($this->data[$key]);
}
public function clean(array $conditions)
{
if (!empty($conditions[Nette\Caching\Cache::ALL])) {
$this->data = [];
}
}
}
@@ -0,0 +1,193 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Caching\Storages;
use Nette;
use Nette\Caching\Cache;
/**
* Memcached storage using memcached extension.
*/
class NewMemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
{
use Nette\SmartObject;
/** @internal cache structure */
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.
* @return bool
*/
public static function isAvailable()
{
return extension_loaded('memcached');
}
public function __construct($host = 'localhost', $port = 11211, $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($host = 'localhost', $port = 11211)
{
if ($this->memcached->addServer($host, $port, 1) === false) {
$error = error_get_last();
throw new Nette\InvalidStateException("Memcached::addServer(): $error[message].");
}
}
/**
* @return \Memcached
*/
public function getConnection()
{
return $this->memcached;
}
public function read($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)
{
$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($key)
{
}
public function write($key, $data, array $dp)
{
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($key)
{
$this->memcached->delete(urlencode($this->prefix . $key), 0);
}
public function clean(array $conditions)
{
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,145 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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;
/**
* @param string $path
*/
public function __construct($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()
{
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($key, array $dependencies)
{
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 ((array) $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)
{
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,137 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
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($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($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)
{
$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($key)
{
}
public function write($key, $data, array $dependencies)
{
$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 ((array) $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($key)
{
$this->pdo->prepare('DELETE FROM cache WHERE key=?')
->execute([$key]);
}
public function clean(array $conditions)
{
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 = (array) $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);
}
}
}