file_cache_준비

This commit is contained in:
2020-06-26 21:24:24 +09:00
parent 1860e1dc43
commit f74a7b31c3
60 changed files with 8621 additions and 109 deletions
+40
View File
@@ -0,0 +1,40 @@
{
"name": "nette/caching",
"description": "⏱ Nette Caching: library with easy-to-use API and many cache backends.",
"keywords": ["nette", "cache", "journal", "sqlite", "memcached"],
"homepage": "https://nette.org",
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"require": {
"php": ">=7.1",
"nette/finder": "^2.4 || ^3.0",
"nette/utils": "^2.4 || ^3.0"
},
"require-dev": {
"nette/tester": "^2.0",
"nette/di": "^v3.0",
"latte/latte": "^2.4",
"tracy/tracy": "^2.4"
},
"suggest": {
"ext-pdo_sqlite": "to use SQLiteStorage or SQLiteJournal"
},
"autoload": {
"classmap": ["src/"]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:
+60
View File
@@ -0,0 +1,60 @@
Licenses
========
Good news! You may use Nette Framework under the terms of either
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
The BSD License is recommended for most projects. It is easy to understand and it
places almost no restrictions on what you can do with the framework. If the GPL
fits better to your project, you can use the framework under this license.
You don't have to notify anyone which license you are using. You can freely
use Nette Framework in commercial projects as long as the copyright header
remains intact.
Please be advised that the name "Nette Framework" is a protected trademark and its
usage has some limitations. So please do not use word "Nette" in the name of your
project or top-level domain, and choose a name that stands on its own merits.
If your stuff is good, it will not take long to establish a reputation for yourselves.
New BSD License
---------------
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of "Nette Framework" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed. In no event shall the copyright owner or contributors be liable for
any direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
GNU General Public License
--------------------------
GPL licenses are very very long, so instead of including them here we offer
you URLs with full text:
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
+260
View File
@@ -0,0 +1,260 @@
Nette Caching
=============
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/caching.svg)](https://packagist.org/packages/nette/caching)
[![Build Status](https://travis-ci.org/nette/caching.svg?branch=master)](https://travis-ci.org/nette/caching)
[![Coverage Status](https://coveralls.io/repos/github/nette/caching/badge.svg?branch=master)](https://coveralls.io/github/nette/caching?branch=master)
[![Latest Stable Version](https://poser.pugx.org/nette/caching/v/stable)](https://github.com/nette/caching/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/caching/blob/master/license.md)
Introduction
------------
Cache accelerates your application by storing data - once hardly retrieved - for future use.
Documentation can be found on the [website](https://doc.nette.org/caching).
If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
Installation
------------
The recommended way to install Nette Caching is via Composer:
```
composer require nette/caching
```
It requires PHP version 7.1 and supports PHP up to 7.4.
Usage
-----
Nette Caching offers a very intuitive API for cache manipulation. Before we show you the first example, we need to think about place where
to store data physically. We can use a database, Memcached server, or the most available storage - hard drive:
```php
// the `temp` directory will be the storage
$storage = new Nette\Caching\Storages\FileStorage('temp');
```
The `Nette\Caching\Storages\FileStorage` storage is very well optimized for performance and in the first place,
it provides full atomicity of operations. What does that mean? When we use cache we can be sure we are not reading a file that is not fully
written yet (by another thread) or that the file gets deleted "under our hands". Using the cache is therefore completely safe.
For manipulation with cache, we use the `Nette\Caching\Cache`:
```php
use Nette\Caching\Cache;
$cache = new Cache($storage); // $storage from the previous example
```
Let's save the contents of the '`$data`' variable under the '`$key`' key:
```php
$cache->save($key, $data);
```
This way, we can read from the cache: (if there is no such item in the cache, the `null` value is returned)
```php
$value = $cache->load($key);
if ($value === null) ...
```
Method `load()` has second parameter `callable` `$fallback`, which is called when there is no such item in the cache. This callback receives the array *$dependencies* by reference, which you can use for setting expiration rules.
```php
$value = $cache->load($key, function(& $dependencies) {
// some calculation
return 15;
});
```
We could delete the item from the cache either by saving null or by calling `remove()` method:
```php
$cache->save($key, null);
// or
$cache->remove($key);
```
It's possible to save any structure to the cache, not only strings. The same applies for keys.
Web applications typically consist of a number of independent parts, and if they all cache data in the same storage (for example the same directory),
sooner or later there would be collisions in names. Nette Framework solves this by splitting the whole storage to sections
(in the `FileStorage` case using subdirectories). Each part of the application uses it's own section with unique name, therefore no collision can occur.
The name of the section can be passed as the second parameter to the `Cache` class constructor. (These sections are often refered to as //cache namespaces//.)
```php
$cache = new Cache($storage, 'htmlOutput');
```
Caching Function Results
------------------------
Caching the result of a function or method call can be achieved using the `call()` method:
```php
$name = $cache->call('gethostbyaddr', $ip);
```
The `gethostbyaddr($ip)` will therefore be called only once and next time, only the value from cache will be returned. Of course, for different `$ip`,
different results are cached.
Output Caching
--------------
The output can be cached not only in templates:
```php
if ($block = $cache->start($key)) {
... printing some data ...
$block->end(); // save the output to the cache
}
```
In case that the output is already present in the cache, the `start()` method prints it and return `null`. Otherwise, it starts to buffer the output and
returns the `$block` object using which we finally save the data to the cache.
Expiration and Invalidation
---------------------------
Here come two problems of storing data in the cache. First, there is a possibility that the storage is completely filled and you cannot save more data inside.
And it may happen that some od the previously saved data will become invalid over time. Therefore, Nette Framework provides a mechanism,
how to limit the validity of data and how to delete them in a controlled way ("to invalidate them", using the framework's terminology).
Data validity is set when saving the data using the third parameter of the `save()` method:
```php
$cache->save($key, $data, array(
Cache::EXPIRE => '20 minutes', // accepts also seconds or a timestamp.
));
```
It's obvious from the code itself, that we saved the data for next 20 minutes. After this period, the cache will report that there is no record
under the '`$key`' key (ie, will return `null`). In fact, you can use any time value that is a valid value for PHP function strToTime().
If we want to extend the validity period with each reading, it can be achieved this way:
```php
$cache->save($key, $data, array(
Cache::EXPIRE => '20 minutes',
Cache::SLIDING => true,
));
```
Very handy is also the ability to let the data expire when a particular file is changed or one of several files.
That can be used for stroring data resulting from parsing these files to the cache. For trouble-free functionality, it's recommended to use absolute paths.
```php
$cache->save($key, $data, array(
Cache::FILES => 'data.yaml', // an array of files can also be specified
));
```
The `Cache::FILES` criteria, of course, can be combined with the time expiration using `Cache::EXPIRE` etc.
The cache can also depend on other cached items. That can be used when we save the whole HTML page in the cache and under different keys, some of its fragments.
As soon as a part changes, the whole page is invalidated.
```php
$cache->save('page', $html, array(
// will expire if frag1 or frag2 expires
Cache::ITEMS => array('frag1', 'frag2'),
));
```
Expiration can be controlled even by your own callbacks:
```php
function controlExpiration($val)
{
return $val;
}
$cache->save($key, $value, array(
Cache::CALLBACKS => array(array('controlExpiration', 1)),
));
```
Expiration Using Tags and Priority
----------------------------------
Very useful invalidation tool are so called //tags//. We can assign a list of tags to each item. For example, suppose we have an HTML page with an article and
comments, which we want to cache. So we specify tags when saving to cache:
```php
$cache->save($articleId, $html, array(
Cache::TAGS => array("article/$articleId", "comments/$articleId"),
));
```
Now, let's move to the administration. Here we have a form for article editing. Together with saving the article to a database, we call the `clean()` command,
which will delete cached items by tag:
```php
$cache->clean(array(
Cache::TAGS => array("article/$articleId"),
));
```
And in the place for adding new comments (or editing them), don't forget to invalidate appropriate tag:
```php
$cache->clean(array(
Cache::TAGS => array("comments/$articleId"),
));
```
What we have achieved? That the HTML cache will invalidate automatically. Whenever someone changes the article with ID of 10, it will force the `article/10`
tag to invalidate and the tagged HTML page in cache is cleared. The same will happen when someone inserts a new comment below the article.
Similar to tags, you can control expiration by priority:
```php
$cache->save($key, $value, array(
Cache::PRIORITY => 50,
));
// all cached items with priority less than or equal to 100 will be removed.
$cache->clean(array(
Cache::PRIORITY => 100,
));
```
Cache Storage
-------------
In addition to already mentioned `FileStorage`, Nette Framework also provides `MemcachedStorage` which stores
data to the `Memcached` server, and also `MemoryStorage` for storing data in memory for duration of the request.
The special `DevNullStorage`, which does precisely nothing, can be used for testing, when we want to eliminate the influence of caching.
Of course, it's possible to create your own storage. The only requirement is to implement the `IStorage` interface.
Concurrent Caching
------------------
Deleting the cache is a common operation when uploading a new application version to the server. At that moment, however, the server gets pretty hard time,
because it has to build a complete new cache. Retrieving some data can be quite difficult, for example the RobotLoader cache building.
And moreover, if, say, 30 requests come in a short period, the resource consumption is even higher.
The solution is to modify application behaviour so that data are created only by one thread and others are waiting. To do this, specify the value as a callback
or use an anonymous function:
```php
$result = $cache->save($key, function() {
return buildData(); // difficult operation
});
```
Framework will ensure that the body of the function will be called only by one thread at once, and other threads will be waiting.
If the thread fails for some reason, another gets chance.
@@ -0,0 +1,57 @@
<?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\Bridges\CacheDI;
use Nette;
/**
* Cache extension for Nette DI.
*/
final class CacheExtension extends Nette\DI\CompilerExtension
{
/** @var string */
private $tempDir;
public function __construct(string $tempDir)
{
$this->tempDir = $tempDir;
}
public function loadConfiguration()
{
$dir = $this->tempDir . '/cache';
Nette\Utils\FileSystem::createDir($dir);
if (!is_writable($dir)) {
throw new Nette\InvalidStateException("Make directory '$dir' writable.");
}
$builder = $this->getContainerBuilder();
if (extension_loaded('pdo_sqlite')) {
$builder->addDefinition($this->prefix('journal'))
->setType(Nette\Caching\Storages\IJournal::class)
->setFactory(Nette\Caching\Storages\SQLiteJournal::class, [$dir . '/journal.s3db']);
}
$builder->addDefinition($this->prefix('storage'))
->setType(Nette\Caching\IStorage::class)
->setFactory(Nette\Caching\Storages\FileStorage::class, [$dir]);
if ($this->name === 'cache') {
if (extension_loaded('pdo_sqlite')) {
$builder->addAlias('nette.cacheJournal', $this->prefix('journal'));
}
$builder->addAlias('cacheStorage', $this->prefix('storage'));
}
}
}
@@ -0,0 +1,136 @@
<?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\Bridges\CacheLatte;
use Latte;
use Nette;
use Nette\Caching\Cache;
/**
* Macro {cache} ... {/cache}
*/
final 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**/
public static function initRuntime(Latte\Runtime\Template $template): void
{
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.
* @return Nette\Caching\OutputHelper|\stdClass
*/
public static function createCache(Nette\Caching\IStorage $cacheStorage, string $key, ?array &$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
*/
public static function endCache(array &$parents, array $args = null): void
{
$helper = array_pop($parents);
if ($helper instanceof Nette\Caching\OutputHelper) {
if (isset($args['dependencies'])) {
$args += $args['dependencies']();
}
if (isset($args['expire'])) {
$args['expiration'] = $args['expire']; // back compatibility
}
$helper->dependencies[Cache::TAGS] = $args['tags'] ?? null;
$helper->dependencies[Cache::EXPIRATION] = $args['expiration'] ?? '+ 7 days';
$helper->end();
}
}
}
+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);
}
}
}