-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy path@home.texy
More file actions
484 lines (320 loc) Β· 17.5 KB
/
@home.texy
File metadata and controls
484 lines (320 loc) Β· 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
Nette Caching
*************
<div class=perex>
Cache speeds up your application by storing data that was once computationally expensive to retrieve, allowing for faster access in the future. We will cover:
- how to use the cache
- how to change the storage backend
- how to correctly invalidate the cache
</div>
Using the cache in Nette is very straightforward, yet it covers sophisticated caching needs. It's designed for performance and 100% durability. It includes adapters for the most common storage backends. It supports tag-based invalidation, time expiration, protection against cache stampede, and more.
Installation
============
Download and install the package using [Composer|best-practices:composer]:
```shell
composer require nette/caching
```
Basic Usage
===========
The core element for working with the cache is the [api:Nette\Caching\Cache] object. We create an instance of it, passing a storage backend object to the constructor. This storage object represents the physical location where data will be stored (database, Memcached, files on disk, etc.). You typically obtain the storage object via [dependency injection |dependency-injection:passing-dependencies] by requesting the type `Nette\Caching\Storage`. You'll learn the essentials in the [Storages section |#Storages].
.[warning]
In version 3.0, the interface still had the `I` prefix, so the name was `Nette\Caching\IStorage`. Furthermore, constants of the `Cache` class were written in uppercase, e.g., `Cache::EXPIRE` instead of `Cache::Expire`.
For the following examples, assume we have an alias `Cache` and a storage instance in the `$storage` variable.
```php
use Nette\Caching\Cache;
$storage = /* ... */; // instance of Nette\Caching\Storage
```
The cache is essentially a *key-value store*, meaning we read and write data using keys, similar to associative arrays. Applications consist of multiple independent parts. If all parts used a single storage (imagine a single directory on disk), key collisions would eventually occur. The Nette Framework addresses this by partitioning the storage space into namespaces (conceptually like subdirectories). Each part of the application then works within its own namespace using a unique name, preventing any collisions.
Specify the namespace name as the second argument to the `Cache` class constructor:
```php
$cache = new Cache($storage, 'Full Html Pages');
```
Now, we can use the `$cache` object to read from and write to the cache. The `load()` method serves both purposes. The first argument is the key, and the second is a PHP callback that gets invoked if the key is not found in the cache. The callback generates the value, returns it, and the `load()` method caches it:
```php
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // expensive computation
return $computedValue;
});
```
If the second parameter is omitted (`$value = $cache->load($key)`), `load()` returns `null` if the item is not found in the cache.
.[tip]
It's great that any serializable structures can be cached, not just strings. The same applies to keys.
To delete an item from the cache, use the `remove()` method:
```php
$cache->remove($key);
```
You can also save an item to the cache using the `$cache->save($key, $value, array $dependencies = [])` method. However, the `load()` approach shown above is generally preferred.
Memoization
===========
Memoization involves caching the result of a function or method call, so the next time it's called with the same arguments, the cached result is returned instead of recalculating it.
Methods and functions can be called in a memoized way using `call(callable $callback, ...$args)`:
```php
$result = $cache->call('gethostbyaddr', $ip);
```
The `gethostbyaddr()` function is thus called only once for each unique `$ip` argument. Subsequent calls with the same `$ip` will return the cached value.
It's also possible to create a memoized wrapper around a method or function, which can then be called later:
```php
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // calculates it the first time
$result = $memoizedFactorial(5); // returns from cache the second time
```
Expiration & Invalidation
=========================
When using caching, it's necessary to address the issue of when previously stored data becomes invalid. Nette Framework provides mechanisms to limit data validity or delete it explicitly (referred to as "invalidation" in the framework's terminology).
Data validity is set at the time of saving, typically using the third parameter of the `save()` method, e.g.:
```php
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
```
Alternatively, it can be set using the `$dependencies` parameter passed by reference to the callback in the `load()` method, e.g.:
```php
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
```
Or by using the 3rd parameter of the `load()` method itself, e.g.:
```php
$value = $cache->load($key, function () {
return ...;
}, [Cache::Expire => '20 minutes']);
```
In the following examples, we'll assume the second variant, utilizing the `$dependencies` variable within the callback.
Expiration
----------
The simplest form of expiration is a time limit. This caches data with a validity of 20 minutes:
```php
// accepts number of seconds or a UNIX timestamp as well
$dependencies[Cache::Expire] = '20 minutes';
```
If you want the validity period to extend with each read (sliding expiration), you can achieve this as follows, but be aware that this increases cache overhead:
```php
$dependencies[Cache::Sliding] = true;
```
A useful option is to have data expire when a specific file or one of several files is modified. This is useful, for example, when caching data derived from processing these files. Use absolute paths.
```php
$dependencies[Cache::Files] = '/path/to/data.yaml';
// or
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
```
We can make a cache item expire when another specific item (or one of several others) expires. This is useful when caching, for instance, an entire HTML page and its fragments under different keys. When a fragment changes, the entire page should be invalidated. If the fragments are stored under keys like `frag1` and `frag2`, use:
```php
$dependencies[Cache::Items] = ['frag1', 'frag2'];
```
Expiration can also be controlled using custom functions or static methods. These are called upon each read to determine if the item is still valid. For example, we can make an item expire whenever the PHP version changes. Create a function that compares the current version with a parameter, and when saving, add an array in the format `[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
];
```
Naturally, all these criteria can be combined. The cache item expires if at least one criterion is no longer met.
```php
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
```
Invalidation Using Tags
-----------------------
Tags provide a very useful invalidation mechanism. We can assign a list of tags (arbitrary strings) to each item stored in the cache. For example, suppose we have an HTML page displaying an article and its comments, which we want to cache. When saving, we specify the relevant tags:
```php
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
```
Now, let's move to the administration section. Here, we have a form for editing articles. Along with saving the article to the database, we call the `clean()` method to delete cached items based on their tag:
```php
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
```
Similarly, when adding a new comment (or editing one), we must remember to invalidate the corresponding tag:
```php
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
```
What have we achieved? Our HTML cache will now be invalidated (deleted) whenever the associated article or its comments change. When editing the article with ID = 10, the tag `article/10` is invalidated, and the cached HTML page carrying this tag is deleted. The same occurs when a new comment is added under the respective article.
.[note]
Tags require a [#Journal].
Invalidation by Priority
------------------------
We can assign priorities to individual cache items. This allows for controlled deletion, for example, when the cache exceeds a certain size limit:
```php
$dependencies[Cache::Priority] = 50;
```
To delete all items with a priority equal to or less than 100:
```php
$cache->clean([
$cache::Priority => 100,
]);
```
.[note]
Priorities require a so-called [#Journal].
Clear Cache
-----------
The `Cache::All` parameter clears everything:
```php
$cache->clean([
$cache::All => true,
]);
```
Bulk Reading
============
For bulk reading and writing to the cache, use the `bulkLoad()` method. Pass it an array of keys, and it returns an array of corresponding values:
```php
$values = $cache->bulkLoad($keys);
```
The `bulkLoad()` method works similarly to `load()`, also accepting a second callback parameter. This callback receives the key of the item being generated:
```php
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // expensive computation
return $computedValue;
});
```
Using with PSR-16 .{data-version:3.3.1}
=======================================
To use Nette Cache with a PSR-16 interface, you can utilize the `PsrCacheAdapter`. It enables seamless integration between Nette Cache and any code or library expecting a PSR-16 compatible cache implementation.
```php
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
```
Now you can use `$psrCache` as a standard PSR-16 cache:
```php
$psrCache->set('key', 'value', 3600); // stores the value for 1 hour
$value = $psrCache->get('key', 'default');
```
The adapter supports all methods defined in PSR-16, including `getMultiple()`, `setMultiple()`, and `deleteMultiple()`.
Output Caching
==============
Output can be captured and cached very elegantly:
```php
if ($capture = $cache->capture($key)) {
echo ... // printing some data
$capture->end(); // save the output to the cache
}
```
If the output is already present in the cache, the `capture()` method prints it and returns `null`, so the `if` condition block is skipped. Otherwise, it starts buffering the output and returns a `$capture` object, which you use to finally save the captured data to the cache via its `end()` method.
.[note]
In version 3.0, this method was named `$cache->start()`.
Caching in Latte
================
Caching in [Latte|latte:] templates is very simple. Just wrap the portion of the template you want to cache with the `{cache}...{/cache}` tags. The cache is automatically invalidated whenever the source template file changes (including any templates included within the cached block). The `{cache}` tags can be nested. When a nested block is invalidated (e.g., via a tag), its parent block is also invalidated.
Within the tag, you can specify keys to which the cache entry will be bound (here, the variable `$id`), set an expiration time, and define [invalidation tags |#Invalidation Using Tags].
```latte
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
```
All these parameters are optional, so you don't need to specify expiration, tags, or even keys.
The use of caching can also be made conditional using `if` β the content will only be cached if the condition is met:
```latte
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
```
Storages
========
A storage is an object representing the physical location where data is stored. We can use a database, a Memcached server, or the most readily available storage: files on disk.
|----------------------
| Storage | Description
|----------------------
| [#FileStorage] | Default storage, saves cache to files on disk.
| [#MemcachedStorage] | Uses a `Memcached` server for storage.
| [#MemoryStorage] | Data is stored temporarily in memory (lost on request end).
| [#SQLiteStorage] | Data is stored in an SQLite database file.
| [#DevNullStorage] | Data isn't actually stored; useful for testing.
You obtain the storage object via [dependency injection |dependency-injection:passing-dependencies] by requesting the type `Nette\Caching\Storage`. By default, Nette provides a `FileStorage` object that stores data in the `cache` subdirectory within the directory for [temporary files |application:bootstrapping#Temporary Files].
You can change the default storage in the configuration:
```neon
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
```
FileStorage
-----------
Writes cache entries to files on disk. The `Nette\Caching\Storages\FileStorage` storage is highly optimized for performance and, crucially, ensures full atomicity of operations. What does this mean? When using the cache, it cannot happen that you read a file that hasn't been completely written by another thread yet, or that someone deletes it while you are reading it. Therefore, using this cache storage is completely safe.
This storage also includes an important built-in feature that prevents an extreme surge in CPU usage when the cache is cleared or is still "cold" (i.e., not yet created). This is known as "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede prevention. It occurs when multiple concurrent requests simultaneously ask for the same cached item (e.g., the result of an expensive SQL query). If the item isn't currently cached, all these processes might start executing the same expensive operation (like the SQL query). This multiplies the server load, and it can even happen that no thread manages to respond within the time limit, the cache doesn't get created, and the application may crash. Fortunately, Nette's cache handles this: when multiple concurrent requests are made for the same item, only the first thread generates it. The other threads wait and then use the result generated by the first one.
Example of creating a `FileStorage`:
```php
// the storage will be the directory '/path/to/temp' on disk
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
```
MemcachedStorage
----------------
The [Memcached |https://memcached.org] server is a high-performance distributed memory object caching system. Its adapter in Nette is `Nette\Caching\Storages\MemcachedStorage`. In the configuration, specify the server's IP address and port if it differs from the standard 11211.
.[caution]
Requires the `memcached` PHP extension.
```neon
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
```
MemoryStorage
-------------
`Nette\Caching\Storages\MemoryStorage` is a storage that holds data within a PHP array. Consequently, the data is lost when the request ends.
SQLiteStorage
-------------
The SQLite database, along with the `Nette\Caching\Storages\SQLiteStorage` adapter, provides a method for caching data within a single file on disk. The configuration specifies the path to this database file.
.[caution]
Requires the `pdo` and `pdo_sqlite` PHP extensions.
```neon
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
```
DevNullStorage
--------------
A special storage implementation is `Nette\Caching\Storages\DevNullStorage`, which doesn't actually store any data. It is therefore suitable for testing purposes when you want to eliminate the effects of caching.
Using Cache in Code
===================
When using caching in your code, there are two main approaches. The first is to obtain the storage object via [dependency injection |dependency-injection:passing-dependencies] and then create the `Cache` object yourself:
```php
use Nette;
class ClassOne
{
private Nette\Caching\Cache $cache;
public function __construct(Nette\Caching\Storage $storage)
{
$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
}
}
```
The second option is to request the `Cache` object directly:
```php
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
```
The `Cache` object must then be defined in the configuration, for example like this:
```neon
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
```
Journal
=======
Nette stores tags and priorities information in a so-called journal. By default, SQLite is used for this purpose via the file `journal.s3db`, and **the `pdo` and `pdo_sqlite` PHP extensions are required.**
You can change the journal implementation in the configuration:
```neon
services:
cache.journal: MyJournal
```
DI Services
===========
These services are added to the DI container:
| Name | Type | Description
|----------------------------------------------------------
| `cache.journal` | [api:Nette\Caching\Storages\Journal] | The cache journal storage
| `cache.storage` | [api:Nette\Caching\Storage] | The primary cache storage
Turning Off Cache
=================
One way to disable caching in your application is to set the storage backend to [#DevNullStorage]:
```neon
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
```
This setting does not affect the caching of Latte templates or the DI container, as these libraries do not utilize `nette/caching` services and manage their caches independently. Furthermore, their caches [do not typically need to be disabled |nette:troubleshooting#How to Disable Cache During Development] during development mode.