Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 5310c2e7f6
commit b306601c72
1098 changed files with 92137 additions and 33228 deletions
+10 -5
View File
@@ -3,7 +3,7 @@
"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"],
"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
"authors": [
{
"name": "David Grudl",
@@ -15,15 +15,16 @@
}
],
"require": {
"php": ">=7.1",
"php": ">=7.2 <8.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"
"latte/latte": "^2.10",
"tracy/tracy": "^2.4",
"phpstan/phpstan": "^0.12"
},
"suggest": {
"ext-pdo_sqlite": "to use SQLiteStorage or SQLiteJournal"
@@ -32,9 +33,13 @@
"classmap": ["src/"]
},
"minimum-stability": "dev",
"scripts": {
"phpstan": "phpstan analyse",
"tester": "tester tests -s"
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.1-dev"
}
}
}
+318 -188
View File
@@ -2,259 +2,389 @@ 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)
[![Tests](https://github.com/nette/caching/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/caching/actions)
[![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!
[Support Me](https://github.com/sponsors/dg)
--------------------------------------------
Do you like Nette Caching? Are you looking forward to the new features?
[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
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.
It requires PHP version 7.2 and supports PHP up to 8.0.
Usage
-----
Basic 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:
The center of work with the cache is the object [Nette\Caching\Cache](https://api.nette.org/3.0/Nette/Caching/Cache.html). We create its instance and pass the so-called storage to the constructor as a parameter. Which is an object representing the place where the data will be physically stored (database, Memcached, files on disk, ...). You will find out all the essentials in [section Storages](#Storages).
```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`:
For the following examples, suppose we have an alias `Cache` and a storage in the variable `$storage`.
```php
use Nette\Caching\Cache;
$cache = new Cache($storage); // $storage from the previous example
$storage // instance of Nette\Caching\IStorage
```
Let's save the contents of the '`$data`' variable under the '`$key`' key:
The cache is actually a *keyvalue store*, so we read and write data under keys just like associative arrays. Applications consist of a number of independent parts, and if they all used one storage (for idea: one directory on a disk), sooner or later there would be a key collision. The Nette Framework solves the problem by dividing the entire space into namespaces (subdirectories). Each part of the program then uses its own space with a unique name and no collisions can occur.
The name of the space is specified as the second parameter of the constructor of the Cache class:
```php
$cache->save($key, $data);
$cache = new Cache($storage, 'Full Html Pages');
```
This way, we can read from the cache: (if there is no such item in the cache, the `null` value is returned)
We can now use object `$cache` to read and write from the cache. The method `load()` is used for both. The first argument is the key and the second is the PHP callback, which is called when the key is not found in the cache. The callback generates a value, returns it and caches it:
```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;
$value = $cache->load($key, function () use ($key) {
$computedValue = ...; // heavy computations
return $computedValue;
});
```
We could delete the item from the cache either by saving null or by calling `remove()` method:
If the second parameter is not specified `$value = $cache->load($key)`, the `null` is returned if the item is not in the cache.
The great thing is that any serializable structures can be cached, not only strings. And the same applies for keys.
The item is cleared from the cache using method `remove()`:
```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.
You can also cache an item using method `$cache->save($key, $value, array $dependencies = [])`. However, the above method using `load()` is preferred.
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//.)
Memoization
===========
Memoization means caching the result of a function or method so you can use it next time instead of calculating the same thing again and again.
Methods and functions can be called memoized using `call(callable $callback, ...$args)`:
```php
$cache = new Cache($storage, 'htmlOutput');
$result = $cache->call('gethostbyaddr', $ip);
```
The function `gethostbyaddr()` is called only once for each parameter `$ip` and the next time the value from the cache will be returned.
It is also possible to create a memoized wrapper for a method or function that can be called later:
```php
function factorial($num)
{
return ...;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // counts it
$result = $memoizedFactorial(5); // returns it from cache
```
Caching Function Results
Expiration & Invalidation
=========================
With caching, it is necessary to address the question that some of the previously saved data will become invalid over time. 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).
The validity of the data is set at the time of saving using the third parameter of the method `save()`, eg:
```php
$cache->save($key, $value, [
Cache::EXPIRE => '20 minutes',
]);
```
Or using the `$dependencies` parameter passed by reference to the callback in the `load()` method, eg:
```php
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::EXPIRE] = '20 minutes';
return ...;
]);
```
In the following examples, we will assume the second variant and thus the existence of a variable `$dependencies`.
Expiration
----------
The simplest exiration is the time limit. Here's how to cache data valid for 20 minutes:
```php
// it also accepts the number of seconds or the UNIX timestamp
$dependencies[Cache::EXPIRE] = '20 minutes';
```
If we want to extend the validity period with each reading, it can be achieved this way, but beware, this will increase the cache overhead:
```php
$dependencies[Cache::SLIDING] = true;
```
The handy option is the ability to let the data expire when a particular file is changed or one of several files. This can be used, for example, for caching data resulting from procession these files. Use absolute paths.
```php
$dependencies[Cache::FILES] = '/path/to/data.yaml';
// nebo
$dependencies[Cache::FILES] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
```
We can let an item in the cache expired when another item (or one of several others) expires. This can be used when we cache the entire HTML page and fragments of it under other keys. Once the snippet changes, the entire page becomes invalid. If we have fragments stored under keys such as `frag1` and `frag2`, we will use:
```php
$dependencies[Cache::ITEMS] = ['frag1', 'frag2'];
```
Expiration can also be controlled using custom functions or static methods, which always decide when reading whether the item is still valid. For example, we can let the item expire whenever the PHP version changes. We will create a function that compares the current version with the parameter, and when saving we will add an array in the form `[function name, ...arguments]` to the dependencies:
```php
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::CALLBACKS] = [
['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false
];
```
Of course, all criteria can be combined. The cache then expires when at least one criterion is not met.
```php
$dependencies[Cache::EXPIRE] = '20 minutes';
$dependencies[Cache::FILES] = '/path/to/data.yaml';
```
Invalidation using Tags
-----------------------
Tags are a very useful invalidation tool. We can assign a list of tags, which are arbitrary strings, to each item stored in the cache. 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
$dependencies[Cache::TAGS] = ["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([
Cache::TAGS => ["article/$articleId"],
]);
```
Likewise, in the place of adding a new comment (or editing a comment), we will not forget to invalidate the relevant tag:
```php
$cache->clean([
Cache::TAGS => ["comments/$articleId"],
]);
```
What have we achieved? That our HTML cache will be invalidated (deleted) whenever the article or comments change. When editing an article with ID = 10, the tag `article/10` is forced to be invalidated and the HTML page carrying the tag is deleted from the cache. The same happens when you insert a new comment under the relevant article.
Tags require [Journal](#Journal).
Invalidation by Priority
------------------------
Caching the result of a function or method call can be achieved using the `call()` method:
We can set the priority for individual items in the cache, and it will be possible to delete them in a controlled way when, for example, the cache exceeds a certain size:
```php
$name = $cache->call('gethostbyaddr', $ip);
$dependencies[Cache::PRIORITY] = 50;
```
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:
Delete all items with a priority equal to or less than 100:
```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->clean([
Cache::PRIORITY => 100,
));
]);
```
Priorities require so-called [Journal](#Journal).
Clear Cache
-----------
The `Cache::ALL` parameter clears everything:
```php
$cache->clean([
Cache::ALL => true,
]);
```
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.
Bulk Reading
============
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:
For bulk reading and writing to cache, the `bulkLoad()` method is used, where we pass an array of keys and obtain an array of values:
```php
$result = $cache->save($key, function() {
return buildData(); // difficult operation
$values = $cache->bulkLoad($keys);
```
Method `bulkLoad()` works similarly to `load()` with the second callback parameter, to which the key of the generated item is passed:
```php
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = ...; // heavy computations
return $computedValue;
});
```
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.
Output Caching
==============
The output can be captured and cached very elegantly:
```php
if ($capture = $cache->start($key)) {
echo ... // printing some data
$capture->end(); // save the output to the cache
}
```
In case that the output is already present in the cache, the `start()` method prints it and returns `null`, so the condition will not be executed. Otherwise, it starts to buffer the output and returns the `$capture` object using which we finally save the data to the cache.
Caching in Latte
================
Caching in templates [Latte](https://latte.nette.org) is very easy, just wrap part of the template with tags `{cache}...{/cache}`. The cache is automatically invalidated when the source template changes (including any included templates within the `{cache}` tags). Tags `{cache}` can be nested, and when a nested block is invalidated (for example, by a tag), the parent block is also invalidated.
In the tag it is possible to specify the keys to which the cache will be bound (here the variable `$id`) and set the expiration and [invalidation tags](#invalidation-using-tags).
```html
{cache $id, expire => '20 minutes', tags => [tag1, tag2]}
...
{/cache}
```
All parameters are optional, so you don't have to specify expiration, tags, or keys.
The use of the cache can also be conditioned by `if` - the content will then be cached only if the condition is met:
```html
{cache $id, if => !$form->isSubmitted()}
{$form}
{/cache}
```
Storages
========
A storage is an object that represents where data is physically stored. We can use a database, a Memcached server, or the most available storage, which are files on disk.
Storage | Description
--------|----------------------
FileStorage | default storage with saving to files on disk
MemcachedStorage | uses the `Memcached` server
MemoryStorage | data are temporarily in memory
SQLiteStorage | data is stored in SQLite database
DevNullStorage | data aren't stored - for testing purposes
FileStorage
-----------
Writes the cache to files on disk. The storage `Nette\Caching\Storages\FileStorage` is very well optimized for performance and above all ensures full atomicity of operations. What does it mean? That when using the cache, it cannot happen that we read a file that has not yet been completely written by another thread, or that someone would delete it "under your hands". The use of the cache is therefore completely safe.
This storage also has an important built-in feature that prevents an extreme increase in CPU usage when the cache is cleared or cold (ie not created). This is [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede) prevention.
It happens that at one moment there are several concurrent requests that want the same thing from the cache (eg the result of an expensive SQL query) and because it is not cached, all processes start executing the same SQL query.
The processor load is multiplied and it can even happen that no thread can respond within the time limit, the cache is not created and the application crashes.
Fortunately, the cache in Nette works in such a way that when there are multiple concurrent requests for one item, it is generated only by the first thread, the others wait and then use the generated result.
Example of creating a FileStorage:
```php
// the storage will be the directory '/path/to/temp' on the disk
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
```
MemcachedStorage
----------------
The server [Memcached](https://memcached.org) is a high-performance distributed storage system whose adapter is `Nette\Caching\Storages\MemcachedStorage`.
Requires PHP extension `memcached`.
```php
$storage = new Nette\Caching\Storages\MemcachedStorage('10.0.0.158');
```
MemoryStorage
-------------
`Nette\Caching\Storages\MemoryStorage` is a storage that stores data in a PHP array and is thus lost when the request is terminated.
```php
$storage = new Nette\Caching\Storages\MemoryStorage;
```
SQLiteStorage
-------------
The SQLite database and adapter `Nette\Caching\Storages\SQLiteStorage` offer a way to cache in a single file on disk. The configuration will specify the path to this file.
Requires PHP extensions `pdo` and `pdo_sqlite`.
```php
$storage = new Nette\Caching\Storages\SQLiteStorage('/path/to/cache.sdb');
```
DevNullStorage
--------------
A special implementation of storage is `Nette\Caching\Storages\DevNullStorage`, which does not actually store data at all. It is therefore suitable for testing if we want to eliminate the effect of the cache.
```php
$storage = new Nette\Caching\Storages\DevNullStorage;
```
Journal
=======
Nette stores tags and priorities in a so-called journal. By default, SQLite and file `journal.s3db` are used for this, and **PHP extensions `pdo` and `pdo_sqlite` are required.**
If you like Nette, **[please make a donation now](https://github.com/sponsors/dg)**. Thank you!
@@ -39,12 +39,12 @@ final class CacheExtension extends Nette\DI\CompilerExtension
if (extension_loaded('pdo_sqlite')) {
$builder->addDefinition($this->prefix('journal'))
->setType(Nette\Caching\Storages\IJournal::class)
->setType(Nette\Caching\Storages\Journal::class)
->setFactory(Nette\Caching\Storages\SQLiteJournal::class, [$dir . '/journal.s3db']);
}
$builder->addDefinition($this->prefix('storage'))
->setType(Nette\Caching\IStorage::class)
->setType(Nette\Caching\Storage::class)
->setFactory(Nette\Caching\Storages\FileStorage::class, [$dir]);
if ($this->name === 'cache') {
+41 -14
View File
@@ -59,8 +59,10 @@ final class CacheMacro implements Latte\IMacro
$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()
->write(
'<?php if (Nette\Bridges\CacheLatte\CacheMacro::createCache($this->global->cacheStorage, %var, $this->global->cacheStack, %node.array?)) /* line %var */ try { ?>',
Nette\Utils\Random::generate(),
$node->startLine
);
}
@@ -72,7 +74,14 @@ final class CacheMacro implements Latte\IMacro
public function nodeClosed(Latte\MacroNode $node)
{
$node->closingCode = Latte\PhpWriter::using($node)
->write('<?php Nette\Bridges\CacheLatte\CacheMacro::endCache($this->global->cacheStack, %node.array?); } ?>');
->write(
'<?php
Nette\Bridges\CacheLatte\CacheMacro::endCache($this->global->cacheStack, %node.array?) /* line %var */;
} catch (\Throwable $ʟ_e) {
Nette\Bridges\CacheLatte\CacheMacro::rollback($this->global->cacheStack); throw $ʟ_e;
} ?>',
$node->startLine
);
}
@@ -94,8 +103,12 @@ final class CacheMacro implements Latte\IMacro
* 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)
{
public static function createCache(
Nette\Caching\Storage $cacheStorage,
string $key,
?array &$parents,
array $args = null
) {
if ($args) {
if (array_key_exists('if', $args) && !$args['if']) {
return $parents[] = new \stdClass;
@@ -119,18 +132,32 @@ final class CacheMacro implements Latte\IMacro
* @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) {
return;
}
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();
}
/**
* @param Nette\Caching\OutputHelper[] $parents
*/
public static function rollback(array &$parents): 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();
$helper->rollback();
}
}
}
@@ -13,12 +13,14 @@ namespace Nette\Caching;
/**
* Cache storage with a bulk read support.
*/
interface IBulkReader
interface BulkReader
{
/**
* Reads from cache in bulk.
* @return array key => value pairs, missing items are omitted
*/
function bulkRead(array $keys): array;
}
class_exists(IBulkReader::class);
+50 -36
View File
@@ -36,14 +36,14 @@ class Cache
/** @internal */
public const NAMESPACE_SEPARATOR = "\x00";
/** @var IStorage */
/** @var Storage */
private $storage;
/** @var string */
private $namespace;
public function __construct(IStorage $storage, string $namespace = null)
public function __construct(Storage $storage, string $namespace = null)
{
$this->storage = $storage;
$this->namespace = $namespace . self::NAMESPACE_SEPARATOR;
@@ -53,7 +53,7 @@ class Cache
/**
* Returns cache storage.
*/
final public function getStorage(): IStorage
final public function getStorage(): Storage
{
return $this->storage;
}
@@ -74,8 +74,7 @@ class Cache
*/
public function derive(string $namespace)
{
$derived = new static($this->storage, $this->namespace . $namespace);
return $derived;
return new static($this->storage, $this->namespace . $namespace);
}
@@ -84,13 +83,19 @@ class Cache
* @param mixed $key
* @return mixed
*/
public function load($key, callable $fallback = null)
public function load($key, callable $generator = null)
{
$data = $this->storage->read($this->generateKey($key));
if ($data === null && $fallback) {
return $this->save($key, function (&$dependencies) use ($fallback) {
return $fallback(...[&$dependencies]);
});
$storageKey = $this->generateKey($key);
$data = $this->storage->read($storageKey);
if ($data === null && $generator) {
$this->storage->lock($storageKey);
try {
$data = $generator(...[&$dependencies]);
} catch (\Throwable $e) {
$this->storage->remove($storageKey);
throw $e;
}
$this->save($key, $data, $dependencies);
}
return $data;
}
@@ -99,7 +104,7 @@ class Cache
/**
* Reads multiple items from the cache.
*/
public function bulkLoad(array $keys, callable $fallback = null): array
public function bulkLoad(array $keys, callable $generator = null): array
{
if (count($keys) === 0) {
return [];
@@ -109,30 +114,31 @@ class Cache
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]);
});
}
}
$result = [];
if (!$this->storage instanceof BulkReader) {
foreach ($keys as $key) {
$result[$key] = $this->load(
$key,
$generator
? function (&$dependencies) use ($key, $generator) {
return $generator(...[$key, &$dependencies]);
}
: null
);
}
return $result;
}
$storageKeys = array_map([$this, 'generateKey'], $keys);
$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]);
} elseif ($generator) {
$result[$key] = $this->load($key, function (&$dependencies) use ($key, $generator) {
return $generator(...[$key, &$dependencies]);
});
} else {
$result[$key] = null;
@@ -206,7 +212,7 @@ class Cache
// 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
$dp[self::CALLBACKS][] = [[self::class, 'checkFile'], $item, @filemtime($item) ?: null]; // @ - stat may fail
}
unset($dp[self::FILES]);
}
@@ -219,7 +225,7 @@ class Cache
// 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)];
$dp[self::CALLBACKS][] = [[self::class, 'checkConst'], $item, constant($item)];
}
unset($dp[self::CONSTS]);
}
@@ -280,15 +286,14 @@ class Cache
public function wrap(callable $function, array $dependencies = null): \Closure
{
return function () use ($function, $dependencies) {
$key = [$function, func_get_args()];
$key = [$function, $args = 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;
return $this->load($key, function (&$deps) use ($function, $args, $dependencies) {
$deps = $dependencies;
return $function(...$args);
});
};
}
@@ -297,7 +302,7 @@ class Cache
* Starts the output cache.
* @param mixed $key
*/
public function start($key): ?OutputHelper
public function capture($key): ?OutputHelper
{
$data = $this->load($key);
if ($data === null) {
@@ -308,6 +313,15 @@ class Cache
}
/**
* @deprecated use capture()
*/
public function start($key): ?OutputHelper
{
return $this->capture($key);
}
/**
* Generates internal cache key.
*/
+10
View File
@@ -48,4 +48,14 @@ class OutputHelper
$this->cache->save($this->key, ob_get_flush(), $dependencies + $this->dependencies);
$this->cache = null;
}
/**
* Stops and throws away the output.
*/
public function rollback(): void
{
ob_end_flush();
$this->cache = null;
}
}
@@ -13,9 +13,8 @@ namespace Nette\Caching;
/**
* Cache storage.
*/
interface IStorage
interface Storage
{
/**
* Read from cache.
* @return mixed
@@ -42,3 +41,6 @@ interface IStorage
*/
function clean(array $conditions): void;
}
class_exists(IStorage::class);
@@ -15,7 +15,7 @@ use Nette;
/**
* Cache dummy storage.
*/
class DevNullStorage implements Nette\Caching\IStorage
class DevNullStorage implements Nette\Caching\Storage
{
use Nette\SmartObject;
+27 -28
View File
@@ -16,7 +16,7 @@ use Nette\Caching\Cache;
/**
* Cache file storage.
*/
class FileStorage implements Nette\Caching\IStorage
class FileStorage implements Nette\Caching\Storage
{
use Nette\SmartObject;
@@ -56,14 +56,14 @@ class FileStorage implements Nette\Caching\IStorage
/** @var string */
private $dir;
/** @var IJournal */
/** @var Journal */
private $journal;
/** @var array */
private $locks;
public function __construct(string $dir, IJournal $journal = null)
public function __construct(string $dir, Journal $journal = null)
{
if (!is_dir($dir)) {
throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
@@ -81,12 +81,9 @@ class FileStorage implements Nette\Caching\IStorage
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;
}
return $meta && $this->verify($meta)
? $this->readData($meta) // calls fclose()
: null;
}
@@ -135,10 +132,12 @@ class FileStorage implements Nette\Caching\IStorage
@mkdir($dir); // @ - directory may already exist
}
$handle = fopen($cacheFile, 'c+b');
if ($handle) {
$this->locks[$key] = $handle;
flock($handle, LOCK_EX);
if (!$handle) {
return;
}
$this->locks[$key] = $handle;
flock($handle, LOCK_EX);
}
@@ -272,12 +271,14 @@ class FileStorage implements Nette\Caching\IStorage
} 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
if (!is_dir($dir)) {
continue;
}
foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
$this->delete((string) $entry);
}
@rmdir($dir); // may already contain new files
}
}
@@ -327,11 +328,7 @@ class FileStorage implements Nette\Caching\IStorage
flock($meta[self::HANDLE], LOCK_UN);
fclose($meta[self::HANDLE]);
if (empty($meta[self::META_SERIALIZED])) {
return $data;
} else {
return unserialize($data);
}
return empty($meta[self::META_SERIALIZED]) ? $data : unserialize($data);
}
@@ -365,12 +362,14 @@ class FileStorage implements Nette\Caching\IStorage
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
if (!$handle) {
return;
}
flock($handle, LOCK_EX);
ftruncate($handle, 0);
flock($handle, LOCK_UN);
fclose($handle);
@unlink($file); // @ - file may not already exist
}
}
@@ -13,9 +13,8 @@ namespace Nette\Caching\Storages;
/**
* Cache journal provider.
*/
interface IJournal
interface Journal
{
/**
* Writes entry information into the journal.
*/
@@ -27,3 +26,6 @@ interface IJournal
*/
function clean(array $conditions): ?array;
}
class_exists(IJournal::class);
@@ -16,7 +16,7 @@ use Nette\Caching\Cache;
/**
* Memcached storage using memcached extension.
*/
class MemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader
{
use Nette\SmartObject;
@@ -32,7 +32,7 @@ class MemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkRea
/** @var string */
private $prefix;
/** @var IJournal */
/** @var Journal */
private $journal;
@@ -45,8 +45,12 @@ class MemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkRea
}
public function __construct(string $host = 'localhost', int $port = 11211, string $prefix = '', IJournal $journal = null)
{
public function __construct(
string $host = 'localhost',
int $port = 11211,
string $prefix = '',
Journal $journal = null
) {
if (!static::isAvailable()) {
throw new Nette\NotSupportedException("PHP extension 'memcached' is not loaded.");
}
@@ -62,7 +66,7 @@ class MemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkRea
public function addServer(string $host = 'localhost', int $port = 11211): void
{
if ($this->memcached->addServer($host, $port, 1) === false) {
if (@$this->memcached->addServer($host, $port, 1) === false) { // @ is escalated to exception
$error = error_get_last();
throw new Nette\InvalidStateException("Memcached::addServer(): $error[message].");
}
@@ -15,7 +15,7 @@ use Nette;
/**
* Memory cache storage.
*/
class MemoryStorage implements Nette\Caching\IStorage
class MemoryStorage implements Nette\Caching\Storage
{
use Nette\SmartObject;
@@ -16,7 +16,7 @@ use Nette\Caching\Cache;
/**
* SQLite based journal.
*/
class SQLiteJournal implements IJournal
class SQLiteJournal implements Journal
{
use Nette\SmartObject;
+14 -8
View File
@@ -16,7 +16,7 @@ use Nette\Caching\Cache;
/**
* SQLite storage.
*/
class SQLiteStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
class SQLiteStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader
{
use Nette\SmartObject;
@@ -56,12 +56,14 @@ class SQLiteStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
{
$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']);
if (!$row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
return;
}
if ($row['slide'] !== null) {
$this->pdo->prepare('UPDATE cache SET expire = ? + slide WHERE key=?')->execute([time(), $key]);
}
return unserialize($row['data']);
}
@@ -92,8 +94,12 @@ class SQLiteStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader
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;
$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 (?, ?, ?, ?)')
+39
View File
@@ -0,0 +1,39 @@
<?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;
if (false) {
/** @deprecated use Nette\Caching\BulkReader */
interface IBulkReader extends BulkReader
{
}
} elseif (!interface_exists(IBulkReader::class)) {
class_alias(BulkReader::class, IBulkReader::class);
}
if (false) {
/** @deprecated use Nette\Caching\Storage */
interface IStorage extends Storage
{
}
} elseif (!interface_exists(IStorage::class)) {
class_alias(Storage::class, IStorage::class);
}
namespace Nette\Caching\Storages;
if (false) {
/** @deprecated use Nette\Caching\Storages\Journal */
interface IJournal extends Journal
{
}
} elseif (!interface_exists(IJournal::class)) {
class_alias(Journal::class, IJournal::class);
}