diff --git a/src/Caching/BulkWriter.php b/src/Caching/BulkWriter.php new file mode 100644 index 00000000..371e72f6 --- /dev/null +++ b/src/Caching/BulkWriter.php @@ -0,0 +1,36 @@ +Similar to write(), but instead of a single key/value item, it works on multiple items specified in items

+ * + * @param array{string, mixed} $items

An array of key/data pairs to store on the server

+ * @param array $dp Global dependencies of each stored value + * @return bool

Returns true on success or false on failure

+ * @throws NotSupportedException + * @throws InvalidStateException + */ + function bulkWrite(array $items, array $dp): bool; + + /** + * Removes multiple items from cache + */ + function bulkRemove(array $keys): void; +} diff --git a/src/Caching/Cache.php b/src/Caching/Cache.php index 245836b5..bad6f78e 100644 --- a/src/Caching/Cache.php +++ b/src/Caching/Cache.php @@ -173,6 +173,56 @@ public function bulkLoad(array $keys, ?callable $generator = null): array } + /** + * Writes multiple items into cache + * + * @param array $items + * @param array|null $dependencies + * @return array Stored items + * + * @throws InvalidArgumentException + */ + public function bulkSave(array $items, ?array $dependencies = null): array + { + $storedItems = []; + + if (!$this->storage instanceof BulkWriter) { + + foreach ($items as $key => $data) { + $storedItems[$key] = $this->save($key, $data, $dependencies); + } + return $storedItems; + } + + $dependencies = $this->completeDependencies($dependencies); + + if (isset($dependencies[self::Expire]) && $dependencies[self::Expire] <= 0) { + $this->storage->bulkRemove(array_map(fn($key): string => $this->generateKey($key), array_keys($items))); + return []; + } + + $removals = []; + $toCache = []; + foreach ($items as $key => $data) { + $cKey = $this->generateKey($key); + + if ($data === null) { + $removals[] = $cKey; + } else { + $storedItems[$key] = $toCache[$cKey] = $data; + } + } + + if (!empty($removals)) { + $this->storage->bulkRemove($removals); + } + + $this->storage->bulkWrite($toCache, $dependencies); + + return $storedItems; + } + + /** * Writes item into the cache. * Dependencies are: diff --git a/src/Caching/Storages/MemcachedStorage.php b/src/Caching/Storages/MemcachedStorage.php index 03af23b3..295783c5 100644 --- a/src/Caching/Storages/MemcachedStorage.php +++ b/src/Caching/Storages/MemcachedStorage.php @@ -16,7 +16,7 @@ /** * Memcached storage using memcached extension. */ -class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader +class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader, Nette\Caching\BulkWriter { /** @internal cache structure */ private const @@ -25,8 +25,6 @@ class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReade MetaDelta = 'delta'; private \Memcached $memcached; - private string $prefix; - private ?Journal $journal; /** @@ -41,15 +39,12 @@ public static function isAvailable(): bool public function __construct( string $host = 'localhost', int $port = 11211, - string $prefix = '', - ?Journal $journal = null, + private string $prefix = '', + private ?Journal $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); @@ -168,12 +163,60 @@ public function write(string $key, $data, array $dp): void } + public function bulkWrite(array $items, array $dp): bool + { + if (isset($dp[Cache::Items])) { + throw new Nette\NotSupportedException('Dependent items are not supported by MemcachedStorage.'); + } + + $records = []; + + $expire = 0; + if (isset($dp[Cache::Expire])) { + $expire = (int) $dp[Cache::Expire]; + } + + foreach ($items as $key => $data) { + $key = urlencode($this->prefix . $key); + $meta = [ + self::MetaData => $data, + ]; + + if (!empty($dp[Cache::Sliding])) { + $meta[self::MetaDelta] = $expire; // sliding time + } + + if (isset($dp[Cache::Callbacks])) { + $meta[self::MetaCallbacks] = $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); + } + + $records[$key] = $meta; + } + + return $this->memcached->setMulti($records, $expire); + } + + public function remove(string $key): void { $this->memcached->delete(urlencode($this->prefix . $key), 0); } + public function bulkRemove(array $keys): void + { + $this->memcached->deleteMulti(array_map(fn($key) => urlencode($this->prefix . $key), $keys), 0); + } + + public function clean(array $conditions): void { if (!empty($conditions[Cache::All])) { diff --git a/tests/Caching/Cache.bulkSave.phpt b/tests/Caching/Cache.bulkSave.phpt new file mode 100644 index 00000000..b5eb295c --- /dev/null +++ b/tests/Caching/Cache.bulkSave.phpt @@ -0,0 +1,53 @@ +bulkSave([1, 2]), 'data'); + Assert::same([1 => 'value1', 2 => 'value2'], $cache->bulkSave([1 => 'value1', 2 => 'value2']), 'data'); + + $data = $cache->bulkLoad([1, 2]); + Assert::same('value1', $data[1]['data']); + Assert::same('value2', $data[2]['data']); +}); + +test('storage with bulk write support', function () { + $storage = new BulkWriteTestStorage; + $cache = new Cache($storage, 'ns'); + Assert::same([1, 2], $cache->bulkSave([1, 2]), 'data'); + Assert::same([1 => 'value1', 2 => 'value2'], $cache->bulkSave([1 => 'value1', 2 => 'value2']), 'data'); + + $data = $cache->bulkLoad([1, 2]); + Assert::same('value1', $data[1]['data']); + Assert::same('value2', $data[2]['data']); +}); + +test('dependencies', function () { + $storage = new BulkWriteTestStorage; + $cache = new Cache($storage, 'ns'); + $dependencies = [Cache::Tags => ['tag']]; + $cache->bulkSave([1 => 'value1', 2 => 'value2'], $dependencies); + + $data = $cache->bulkLoad([1, 2]); + Assert::same($dependencies, $data[1]['dependencies']); + Assert::same($dependencies, $data[2]['dependencies']); + + $cache->clean($dependencies); + + Assert::same([1 => null, 2 => null], $cache->bulkLoad([1, 2])); +}); diff --git a/tests/Caching/Cache.php b/tests/Caching/Cache.php index 57320d8c..e918f56a 100644 --- a/tests/Caching/Cache.php +++ b/tests/Caching/Cache.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Nette\Caching\BulkWriter; use Nette\Caching\IBulkReader; use Nette\Caching\IStorage; @@ -37,6 +38,25 @@ public function remove(string $key): void public function clean(array $conditions): void { + if (!empty($conditions[Nette\Caching\Cache::All])) { + $this->data = []; + return; + } + + //unset based by tags + if (!empty($conditions[Nette\Caching\Cache::Tags])) { + $unsets = []; + foreach ($this->data as $key => $data) { + $tags = $data['dependencies'][Nette\Caching\Cache::Tags] ?? null; + if (array_intersect($conditions[Nette\Caching\Cache::Tags], $tags)) { + $unsets[$key] = $key; + } + } + + foreach ($unsets as $unsetKey) { + unset($this->data[$unsetKey]); + } + } } } @@ -55,3 +75,35 @@ public function bulkRead(array $keys): array return $result; } } + +class BulkWriteTestStorage extends TestStorage implements BulkWriter +{ + public function bulkRead(array $keys): array + { + $result = []; + foreach ($keys as $key) { + $data = $this->read($key); + if ($data !== null) { + $result[$key] = $data; + } + } + + return $result; + } + + + public function bulkRemove(array $keys): void + { + + } + + + public function bulkWrite($items, array $dp): bool + { + foreach ($items as $key => $data) { + $this->write($key, $data, $dp); + } + + return true; + } +} diff --git a/tests/Storages/Memcached.bulkWrite.phpt b/tests/Storages/Memcached.bulkWrite.phpt new file mode 100644 index 00000000..49616675 --- /dev/null +++ b/tests/Storages/Memcached.bulkWrite.phpt @@ -0,0 +1,37 @@ +bulkSave(["foo" => "bar"]); +Assert::same(['foo' => 'bar', 'lorem' => null], $cache->bulkLoad(['foo', 'lorem'])); + +//tags +$dependencies = [Cache::Tags => ['tag']]; +$cache->bulkSave(["foo" => "bar"], $dependencies); +Assert::same(['foo' => 'bar'], $cache->bulkLoad(['foo'])); +$cache->clean($dependencies); +Assert::same(['foo' => null], $cache->bulkLoad(['foo'])); \ No newline at end of file