Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleaning namespace in SQLite #53

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,32 @@ Nette Caching
[![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.

Install it using Composer:
Documentation can be found on the [website](https://doc.nette.org/caching).


Installation
------------

The recommended way to install Nette Caching is via Composer:

```
composer require nette/caching
```

The last stable release requires PHP version 5.6 or newer (is compatible with PHP 7.0 and 7.1). The dev-master version requires PHP 7.1.
It requires PHP version 5.6 and supports PHP up to 7.2. The dev-master version requires PHP 7.1.

Nette offers a very intuitive API for cache manipulation. After all, you wouldn't expect anything else, right? ;-)
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:

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
Expand Down Expand Up @@ -82,7 +95,7 @@ $cache = new Cache($storage, 'htmlOutput');


Caching Function Results
-------------------------
------------------------

Caching the result of a function or method call can be achieved using the `call()` method:

Expand All @@ -94,7 +107,7 @@ The `gethostbyaddr($ip)` will therefore be called only once and next time, only
different results are cached.

Output Caching
------------------
--------------

The output can be cached not only in templates:

Expand Down Expand Up @@ -217,9 +230,8 @@ $cache->clean(array(
```



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.
Expand All @@ -238,8 +250,8 @@ The solution is to modify application behaviour so that data are created only by
or use an anonymous function:

```php
$result = $cache->save($key, function() { // or callback(...)
return buildData(); // difficult operation
$result = $cache->save($key, function() {
return buildData(); // difficult operation
});
```

Expand Down
40 changes: 29 additions & 11 deletions src/Caching/Storages/SQLiteStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function __construct($path)
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->pdo->exec('
PRAGMA foreign_keys = ON;
PRAGMA case_sensitive_like = ON;

CREATE TABLE IF NOT EXISTS cache (
key BLOB NOT NULL PRIMARY KEY,
data BLOB NOT NULL,
Expand All @@ -58,6 +60,7 @@ public function __construct($path)
*/
public function read(string $key)
{
$key = self::sanitize($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)) {
Expand All @@ -75,6 +78,7 @@ public function read(string $key)
*/
public function bulkRead(array $keys): array
{
$keys = array_map([self::class, 'sanitize'], $keys);
$stmt = $this->pdo->prepare('SELECT key, data, slide FROM cache WHERE key IN (?' . str_repeat(',?', count($keys) - 1) . ') AND (expire IS NULL OR expire >= ?)');
$stmt->execute(array_merge($keys, [time()]));
$result = [];
Expand All @@ -83,7 +87,7 @@ public function bulkRead(array $keys): array
if ($row['slide'] !== null) {
$updateSlide[] = $row['key'];
}
$result[$row['key']] = unserialize($row['data']);
$result[str_replace("\x01", Cache::NAMESPACE_SEPARATOR, $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) . ')');
Expand All @@ -106,6 +110,7 @@ public function lock(string $key): void
*/
public function write(string $key, $data, array $dependencies): void
{
$key = self::sanitize($key);
$expire = isset($dependencies[Cache::EXPIRATION]) ? $dependencies[Cache::EXPIRATION] + time() : null;
$slide = isset($dependencies[Cache::SLIDING]) ? $dependencies[Cache::EXPIRATION] : null;

Expand All @@ -131,7 +136,7 @@ public function write(string $key, $data, array $dependencies): void
public function remove(string $key): void
{
$this->pdo->prepare('DELETE FROM cache WHERE key=?')
->execute([$key]);
->execute([self::sanitize($key)]);
}


Expand All @@ -142,18 +147,31 @@ public function clean(array $conditions): void
{
if (!empty($conditions[Cache::ALL])) {
$this->pdo->prepare('DELETE FROM cache')->execute();
return;
}

} else {
$sql = 'DELETE FROM cache WHERE expire < ?';
$args = [time()];
$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);
}
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);
if (!empty($conditions[Cache::NAMESPACES])) {
foreach ($conditions[Cache::NAMESPACES] as $namespace) {
$sql .= ' OR key LIKE ?';
$args[] = self::sanitize($namespace . Cache::NAMESPACE_SEPARATOR . '%');
}
}

$this->pdo->prepare($sql)->execute($args);
}


private function sanitize($key)
{
return str_replace(Cache::NAMESPACE_SEPARATOR, "\x01", $key);
}
}
94 changes: 94 additions & 0 deletions tests/Storages/SQLiteStorage.clean-namespace.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/**
* Test: Nette\Caching\Storages\SQLiteStorage clean with Cache::NAMESPACE
* @phpExtension pdo_sqlite
*/

declare(strict_types=1);

use Nette\Caching\Cache;
use Nette\Caching\Storages\SQLiteStorage;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';

$storage = new SQLiteStorage(':memory:');

/*
* Create SQLiteStorage cache without namespace and some with namespaces
*/
$cacheA = new Cache($storage);
$cacheB = new Cache($storage, 'C' . Cache::NAMESPACE_SEPARATOR . 'A');
$cacheC = new Cache($storage, 'C');
$cacheD = new Cache($storage, 'D');

/*
* Fill with data
*/
$cacheA->save('test1', 'David');
$cacheA->save('test2', 'Grudl');

$cacheB->save('test1', 'Barry');
$cacheB->save('test2', 'Allen');

$cacheC->save('test1', 'Oliver');
$cacheC->save('test2', 'Queen');

$cacheD->save('test1', 'Bruce');
$cacheD->save('test2', 'Wayne');


/*
* Check if fill wass successfull
*/
Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

Assert::same('Barry', $cacheB->load('test1'));
Assert::same('Allen', $cacheB->load('test2'));

Assert::same('Oliver', $cacheC->load('test1'));
Assert::same('Queen', $cacheC->load('test2'));

Assert::same('Bruce', $cacheD->load('test1'));
Assert::same('Wayne', $cacheD->load('test2'));


/*
* Clean one namespace
*/
$storage->clean([Cache::NAMESPACES => [$cacheB->getNamespace()]]);

Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

// Only these should be null now
Assert::null($cacheB->load('test1'));
Assert::null($cacheB->load('test2'));

Assert::same('Oliver', $cacheC->load('test1'));
Assert::same('Queen', $cacheC->load('test2'));

Assert::same('Bruce', $cacheD->load('test1'));
Assert::same('Wayne', $cacheD->load('test2'));


/*
* Test cleaning multiple namespaces
*/
$storage->clean([Cache::NAMESPACES => [$cacheC->getNamespace(), $cacheD->getNamespace()]]);

Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

// All other should be null
Assert::null($cacheB->load('test1'));
Assert::null($cacheB->load('test2'));

Assert::null($cacheC->load('test1'));
Assert::null($cacheC->load('test2'));

Assert::null($cacheD->load('test1'));
Assert::null($cacheD->load('test2'));