├── .gitignore
├── phpstan.neon
├── phpcs.xml
├── docs
├── implementations
│ ├── notcache.md
│ ├── memory.md
│ ├── apcu.md
│ ├── memcached.md
│ ├── predis.md
│ ├── redis.md
│ ├── chain.md
│ ├── mysqli.md
│ ├── mongodb.md
│ ├── phpfile.md
│ └── file.md
└── performance.md
├── .formatter.yml
├── tests
├── performance
│ ├── Apc.php
│ ├── File.php
│ ├── NoCache.php
│ ├── Mongo.php
│ └── common.php
├── FileTest.php
├── PhpFileTest.php
├── FileTtlFileTest.php
├── MemoryTest.php
├── ApcuCacheTest.php
├── FileTrieTest.php
├── PredisTest.php
├── RedisTest.php
├── MongoDBTest.php
├── MemcachedTest.php
├── NotCacheTest.php
├── MysqliTest.php
├── AbstractCacheTest.php
└── ChainTest.php
├── src
├── Exception
│ ├── BadMethodCallException.php
│ ├── CacheException.php
│ ├── UnexpectedValueException.php
│ └── InvalidArgumentException.php
├── Option
│ ├── PrefixTrait.php
│ ├── TtlTrait.php
│ ├── InitializeTrait.php
│ └── FilenameTrait.php
├── Packer
│ ├── PackerInterface.php
│ ├── NopPacker.php
│ ├── JsonPacker.php
│ ├── SerializePacker.php
│ ├── MongoDBBinaryPacker.php
│ └── PackingTrait.php
├── CacheInterface.php
├── File
│ ├── BasicFilename.php
│ └── TrieFilename.php
├── NotCache.php
├── Apcu.php
├── PhpFile.php
├── Memory.php
├── Chain.php
├── File.php
├── Redis.php
├── AbstractFile.php
├── Memcached.php
├── Predis.php
├── MongoDB.php
├── AbstractCache.php
└── Mysqli.php
├── .scrutinizer.yml
├── LICENSE
├── phpunit.xml.dist
├── composer.json
├── .github
└── workflows
│ └── php.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /composer.lock
3 | /tests/config.json
4 | /vendor
5 | /.idea
6 | /TODO.md
7 | *~
8 | *.swp
9 | /.phpunit.result.cache
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 3
3 | paths:
4 | - src
5 | reportUnmatchedIgnoredErrors: false
6 | ignoreErrors:
7 | - /^Variable property access/
8 |
9 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/implementations/notcache.md:
--------------------------------------------------------------------------------
1 | # NotCache
2 |
3 | A [Null object](https://sourcemaking.com/design_patterns/null_object) that
4 | correctly implements the PSR-16 interface, but does not actually cache
5 | anything.
6 |
7 | ``` php
8 | use Desarrolla2\Cache\NotCache;
9 |
10 | $cache = new NotCache();
11 | ```
12 |
13 | It doesn't use any options or packers.
14 |
--------------------------------------------------------------------------------
/.formatter.yml:
--------------------------------------------------------------------------------
1 | use-sort:
2 | group:
3 | - _main
4 | group-type: each
5 | sort-type: alph
6 | sort-direction: asc
7 |
8 | header: |
9 | /*
10 | * This file is part of the Cache package.
11 | *
12 | * Copyright (c) Daniel González
13 | *
14 | * For the full copyright and license information, please view the LICENSE
15 | * file that was distributed with this source code.
16 | *
17 | * @author Daniel González
18 | */
19 |
--------------------------------------------------------------------------------
/tests/performance/Apc.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | require_once __DIR__.'/../bootstrap.php';
15 |
16 | use Desarrolla2\Cache\Cache;
17 | use Desarrolla2\Cache\Adapter\Apcu;
18 |
19 | $cache = new Cache(new Apcu());
20 |
21 | require_once __DIR__.'/common.php';
22 |
--------------------------------------------------------------------------------
/tests/performance/File.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | require_once __DIR__.'/../bootstrap.php';
15 |
16 | use Desarrolla2\Cache\Cache;
17 | use Desarrolla2\Cache\Adapter\File;
18 |
19 | $cache = new Cache(new File('/tmp'));
20 |
21 | require_once __DIR__.'/common.php';
22 |
--------------------------------------------------------------------------------
/tests/performance/NoCache.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | require_once __DIR__.'/../bootstrap.php';
15 |
16 | use Desarrolla2\Cache\Cache;
17 | use Desarrolla2\Cache\Adapter\NotCache;
18 |
19 | $cache = new Cache(new NotCache());
20 |
21 | require_once __DIR__.'/common.php';
22 |
--------------------------------------------------------------------------------
/tests/performance/Mongo.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | require_once __DIR__.'/../bootstrap.php';
15 |
16 | use Desarrolla2\Cache\Cache;
17 | use Desarrolla2\Cache\Adapter\Mongo;
18 |
19 | $cache = new Cache(new Mongo('mongodb://localhost:27017'));
20 |
21 | require_once __DIR__.'/common.php';
22 |
--------------------------------------------------------------------------------
/src/Exception/BadMethodCallException.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Exception;
17 |
18 | use Psr\SimpleCache\CacheException as PsrCacheException;
19 |
20 | /**
21 | * Exception bad method calls
22 | */
23 | class BadMethodCallException extends \BadMethodCallException implements PsrCacheException
24 | {
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/CacheException.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Exception;
17 |
18 | use Psr\SimpleCache\CacheException as PsrCacheException;
19 |
20 | /**
21 | * Interface used for all types of exceptions thrown by the implementing library.
22 | */
23 | class CacheException extends \RuntimeException implements PsrCacheException
24 | {
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/UnexpectedValueException.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Exception;
17 |
18 | use Psr\SimpleCache\CacheException as PsrCacheException;
19 |
20 | /**
21 | * Exception for unexpected values when reading from cache.
22 | */
23 | class UnexpectedValueException extends \UnexpectedValueException implements PsrCacheException
24 | {
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Exception;
17 |
18 | use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException;
19 |
20 | /**
21 | * Exception for invalid cache arguments.
22 | */
23 | class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException
24 | {
25 | }
26 |
--------------------------------------------------------------------------------
/docs/implementations/memory.md:
--------------------------------------------------------------------------------
1 | # Memory
2 |
3 | Store the cache in process memory _(in other words in an array)_. Cache Memory
4 | is removed when the PHP process exist. Also it is not shared between different
5 | processes.
6 |
7 | ``` php
8 | use Desarrolla2\Cache\Memory as MemoryCache;
9 |
10 | $cache = new MemoryCache();
11 | ```
12 |
13 | ### Options
14 |
15 | | name | type | default | |
16 | | --------- | ---- | ------- | ------------------------------------- |
17 | | ttl | int | null | Maximum time to live in seconds |
18 | | limit | int | null | Maximum items in cache |
19 | | prefix | string | "" | Key prefix |
20 |
21 | ### Packer
22 |
23 | By default the [`SerializePacker`](../packers/serialize.md) is used.
24 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | #language: php
2 | checks:
3 | php: true
4 | filter:
5 | excluded_paths:
6 | - tests
7 | build:
8 | nodes:
9 | analysis:
10 | environment:
11 | php:
12 | version: 7.4
13 | pecl_extensions:
14 | - apcu
15 | - mongodb
16 | - memcached
17 | mysql: false
18 | postgresql: false
19 | redis: false
20 | mongodb: false
21 | tests:
22 | override:
23 | - phpcs-run src
24 | -
25 | command: vendor/bin/phpstan analyze --error-format=checkstyle | sed '/^\s*$/d' > phpstan-checkstyle.xml
26 | analysis:
27 | file: phpstan-checkstyle.xml
28 | format: 'general-checkstyle'
29 | - php-scrutinizer-run
30 | tools:
31 | external_code_coverage: true
32 |
--------------------------------------------------------------------------------
/docs/implementations/apcu.md:
--------------------------------------------------------------------------------
1 | # Apcu
2 |
3 | Use [APCu cache](http://php.net/manual/en/book.apcu.php) to cache to shared
4 | memory.
5 |
6 | ``` php
7 | use Desarrolla2\Cache\Apcu as ApcuCache;
8 |
9 | $cache = new ApcuCache();
10 | ```
11 |
12 | _Note: by default APCu uses the time at the beginning of a request for ttl. In
13 | some cases, like with a long running script, this can be a problem. You can
14 | change this behaviour `ini_set('apc.use_request_time', false)`._
15 |
16 | ### Options
17 |
18 | | name | type | default | |
19 | | --------- | ---- | ------- | ------------------------------------- |
20 | | ttl | int | null | Maximum time to live in seconds |
21 | | prefix | string | "" | Key prefix |
22 |
23 | ### Packer
24 |
25 | By default the [`NopPacker`](../packers/nop.md) is used.
26 |
--------------------------------------------------------------------------------
/docs/implementations/memcached.md:
--------------------------------------------------------------------------------
1 | # Memcached
2 |
3 | Store cache to [Memcached](https://memcached.org/). Memcached is a high
4 | performance distributed caching system.
5 |
6 | ``` php
7 | use Desarrolla2\Cache\Memcached as MemcachedCache;
8 | use Memcached;
9 |
10 | $server = new Memcached();
11 | // configure it here
12 |
13 | $cache = new MemcachedCache($server);
14 | ```
15 |
16 | This implementation uses the [memcached](https://php.net/memcached) php
17 | extension. The (alternative) memcache extension is not supported.
18 |
19 | ### Options
20 |
21 | | name | type | default | |
22 | | --------- | ---- | ------- | ------------------------------------- |
23 | | ttl | int | null | Maximum time to live in seconds |
24 | | prefix | string | "" | Key prefix |
25 |
26 | ### Packer
27 |
28 | By default the [`NopPacker`](../packers/nop.md) is used.
29 |
--------------------------------------------------------------------------------
/tests/FileTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\File as FileCache;
17 | use org\bovigo\vfs\vfsStream;
18 | use org\bovigo\vfs\vfsStreamDirectory;
19 |
20 | /**
21 | * FileTest
22 | */
23 | class FileTest extends AbstractCacheTest
24 | {
25 | /**
26 | * @var vfsStreamDirectory
27 | */
28 | private $root;
29 |
30 | protected $skippedTests = [
31 | 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes'
32 | ];
33 |
34 | public function createSimpleCache()
35 | {
36 | $this->root = vfsStream::setup('cache');
37 |
38 | return new FileCache(vfsStream::url('cache'));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/PhpFileTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\PhpFile as PhpFileCache;
17 | use org\bovigo\vfs\vfsStream;
18 | use org\bovigo\vfs\vfsStreamDirectory;
19 |
20 | /**
21 | * FileTest with PhpPacker
22 | */
23 | class PhpFileTest extends AbstractCacheTest
24 | {
25 | /**
26 | * @var vfsStreamDirectory
27 | */
28 | private $root;
29 |
30 | protected $skippedTests = [
31 | 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes'
32 | ];
33 |
34 | public function createSimpleCache()
35 | {
36 | $this->root = vfsStream::setup('cache');
37 |
38 | return new PhpFileCache(vfsStream::url('cache'));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/docs/implementations/predis.md:
--------------------------------------------------------------------------------
1 | # Predis
2 |
3 | Cache using a [redis server](https://redis.io/). Redis is an open source,
4 | in-memory data structure store, used as a database, cache and message broker.
5 |
6 | You must provide a `Predis\Client` object to the constructor.
7 |
8 | ```php
9 | use Desarrolla2\Cache\Predis as PredisCache;
10 | use Predis\Client as PredisClient;
11 |
12 | $client = new PredisClient('tcp://localhost:6379');
13 | $cache = new PredisCache($client);
14 | ```
15 |
16 | ### Installation
17 |
18 | Requires the [`predis`](https://github.com/nrk/predis/wiki) library.
19 |
20 | composer require predis/predis
21 |
22 | ### Options
23 |
24 | | name | type | default | |
25 | | --------- | ---- | ------- | ------------------------------------- |
26 | | ttl | int | null | Maximum time to live in seconds |
27 | | prefix | string | "" | Key prefix |
28 |
29 | ### Packer
30 |
31 | By default the [`SerializePacker`](../packers/serialize.md) is used.
32 |
--------------------------------------------------------------------------------
/docs/implementations/redis.md:
--------------------------------------------------------------------------------
1 | # Redis
2 |
3 | Cache using a [redis server](https://redis.io/). Redis is an open source,
4 | in-memory data structure store, used as a database, cache and message broker.
5 |
6 | Uses the [Redis PHP extension](https://github.com/phpredis/phpredis).
7 |
8 | You must provide a `Redis` object to the constructor.
9 |
10 | ```php
11 | use Desarrolla2\Cache\Redis as RedisCache;
12 | use Redis;
13 |
14 | $client = new Redis();
15 | $cache = new RedisCache($client);
16 | ```
17 |
18 | ### Installation
19 |
20 | Requires the [`redis`](https://github.com/phpredis/phpredis) PHP extension from PECL.
21 |
22 | pickle install redis
23 |
24 | ### Options
25 |
26 | | name | type | default | |
27 | | --------- | ---- | ------- | ------------------------------------- |
28 | | ttl | int | null | Maximum time to live in seconds |
29 | | prefix | string | "" | Key prefix |
30 |
31 | ### Packer
32 |
33 | By default the [`SerializePacker`](../packers/serialize.md) is used.
34 |
--------------------------------------------------------------------------------
/src/Option/PrefixTrait.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Option;
17 |
18 | /**
19 | * Prefix option
20 | */
21 | trait PrefixTrait
22 | {
23 | /**
24 | * @var string
25 | */
26 | protected $prefix = '';
27 |
28 |
29 | /**
30 | * Set the key prefix
31 | *
32 | * @param string $prefix
33 | * @return void
34 | */
35 | protected function setPrefixOption(string $prefix): void
36 | {
37 | $this->prefix = $prefix;
38 | }
39 |
40 | /**
41 | * Get the key prefix
42 | *
43 | * @return string
44 | */
45 | protected function getPrefixOption(): string
46 | {
47 | return $this->prefix;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/FileTtlFileTest.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | namespace Desarrolla2\Test\Cache;
16 |
17 | use Desarrolla2\Cache\File as FileCache;
18 | use org\bovigo\vfs\vfsStream;
19 | use org\bovigo\vfs\vfsStreamDirectory;
20 |
21 | /**
22 | * FileTest
23 | */
24 | class FileTtlFileTest extends AbstractCacheTest
25 | {
26 | /**
27 | * @var vfsStreamDirectory
28 | */
29 | private $root;
30 |
31 | protected $skippedTests = [
32 | 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes'
33 | ];
34 |
35 | public function createSimpleCache()
36 | {
37 | $this->root = vfsStream::setup('cache');
38 |
39 | return (new FileCache(vfsStream::url('cache')))
40 | ->withOption('ttl-strategy', 'file');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2013 Desarrolla2 - http://desarrolla2.com
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/src/Packer/PackerInterface.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache\Packer;
18 |
19 | /**
20 | * Interface for packer / unpacker
21 | */
22 | interface PackerInterface
23 | {
24 | /**
25 | * Get cache type (might be used as file extension)
26 | *
27 | * @return string
28 | */
29 | public function getType();
30 |
31 | /**
32 | * Pack the value
33 | *
34 | * @param mixed $value
35 | * @return string|mixed
36 | */
37 | public function pack($value);
38 |
39 | /**
40 | * Unpack the value
41 | *
42 | * @param string|mixed $packed
43 | * @return string
44 | * @throws \UnexpectedValueException if the value can't be unpacked
45 | */
46 | public function unpack($packed);
47 | }
48 |
--------------------------------------------------------------------------------
/tests/MemoryTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Memory as MemoryCache;
17 |
18 | /**
19 | * MemoryTest
20 | */
21 | class MemoryTest extends AbstractCacheTest
22 | {
23 | public function createSimpleCache()
24 | {
25 | return new MemoryCache();
26 | }
27 |
28 | public function tearDown(): void
29 | {
30 | // No need to clear cache, as the adapters don't persist between tests.
31 | }
32 |
33 | public function testExceededLimit()
34 | {
35 | $cache = $this->createSimpleCache()->withOption('limit', 1);
36 |
37 | $cache->set('foo', 1);
38 | $this->assertTrue($cache->has('foo'));
39 |
40 | $cache->set('bar', 1);
41 | $this->assertFalse($cache->has('foo'));
42 | $this->assertTrue($cache->has('bar'));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/implementations/chain.md:
--------------------------------------------------------------------------------
1 | # Chain
2 |
3 | The Cache chain allows you to use multiple implementations to store cache. For
4 | instance, you can use both fast volatile (in-memory) storage and slower
5 | non-volatile (disk) storage. Alternatively you can have a local storage
6 | as well as a shared storage service.
7 |
8 | ``` php
9 | use Desarrolla2\Cache\Chain as CacheChain;
10 | use Desarrolla2\Cache\Memory as MemoryCache;
11 | use Desarrolla2\Cache\Predis as PredisCache;
12 |
13 | $cache = new CacheChain([
14 | (new MemoryCache())->withOption('ttl', 3600),
15 | (new PredisCache())->withOption('ttl', 10800)
16 | ]);
17 | ```
18 |
19 | The Chain cache implementation doesn't use any option. It uses the `Nop` packer
20 | by default.
21 |
22 | Typically it's useful to specify a maximum `ttl` for each implementation. This
23 | means that the volatile memory only holds items that are used often.
24 |
25 | The following actions propogate to all cache adapters in the chain
26 |
27 | * `set`
28 | * `setMultiple`
29 | * `delete`
30 | * `deleteMultiple`
31 | * `clear`
32 |
33 | For the following actions all nodes are tried in sequence
34 |
35 | * `has`
36 | * `get`
37 | * `getMultiple`
--------------------------------------------------------------------------------
/tests/performance/common.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 |
15 | //build test data outside of timing loop
16 | $data = [];
17 | for ($i = 1; $i <= 10000; $i++) {
18 | $data[$i] = md5($i);
19 | }
20 |
21 | $timer = new \Desarrolla2\Timer\Timer(new \Desarrolla2\Timer\Formatter\Human());
22 | for ($i = 1; $i <= 10000; $i++) {
23 | $cache->set($data[$i], $data[$i], 3600);
24 | }
25 | $timer->mark('10.000 set');
26 | for ($i = 1; $i <= 10000; $i++) {
27 | $cache->has($data[$i]);
28 | }
29 | $timer->mark('10.000 has');
30 | for ($i = 1; $i <= 10000; $i++) {
31 | $cache->get($data[$i]);
32 | }
33 | $timer->mark('10.000 get');
34 | for ($i = 1; $i <= 10000; $i++) {
35 | $cache->has($data[$i]);
36 | $cache->get($data[$i]);
37 | }
38 | $timer->mark('10.000 has+get combos');
39 |
40 | $benchmarks = $timer->getAll();
41 | foreach ($benchmarks as $benchmark) {
42 | ld($benchmark);
43 | }
44 |
--------------------------------------------------------------------------------
/tests/ApcuCacheTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Apcu as ApcuCache;
17 |
18 | /**
19 | * ApcuCacheTest
20 | */
21 | class ApcuCacheTest extends AbstractCacheTest
22 | {
23 | public static function setUpBeforeClass(): void
24 | {
25 | // Required to check the TTL for new entries
26 | ini_set('apc.use_request_time', false);
27 | }
28 |
29 | public function createSimpleCache()
30 | {
31 | if (!extension_loaded('apcu')) {
32 | $this->markTestSkipped(
33 | 'The APCu extension is not available.'
34 | );
35 | }
36 | if (!ini_get('apc.enable_cli')) {
37 | $this->markTestSkipped(
38 | 'You need to enable apc.enable_cli'
39 | );
40 | }
41 |
42 | return new ApcuCache();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/FileTrieTest.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | namespace Desarrolla2\Test\Cache;
16 |
17 | use Desarrolla2\Cache\File as FileCache;
18 | use Desarrolla2\Cache\File\TrieFilename;
19 | use org\bovigo\vfs\vfsStream;
20 | use org\bovigo\vfs\vfsStreamDirectory;
21 |
22 | /**
23 | * FileTest with Trie structure
24 | */
25 | class FileTrieTest extends AbstractCacheTest
26 | {
27 | /**
28 | * @var vfsStreamDirectory
29 | */
30 | private $root;
31 |
32 | protected $skippedTests = [
33 | 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes'
34 | ];
35 |
36 | public function createSimpleCache()
37 | {
38 | $this->root = vfsStream::setup('cache');
39 |
40 | return (new FileCache(vfsStream::url('cache')))
41 | ->withOption('filename', new TrieFilename('%s.php.cache',4));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/performance.md:
--------------------------------------------------------------------------------
1 | # Performance test
2 |
3 | Here are my performance tests, you can view the results ordered from faster to slower.
4 |
5 | | Adapter | 10.000 set | 10.000 has | 10.000 get |
6 | | :-------------- | -----------: | -----------: | ---------: |
7 | | NoCache | 0.0637 | 0.0482 | 0.0488 |
8 | | Apcu | 0.0961 | 0.0556 | 0.0770 |
9 | | File | 0.6881 | 0.3426 | 0.3107 |
10 | | Mongo | 13.8144 | 30.0203 | 24.4214 |
11 |
12 |
13 | ## how i run the test?
14 |
15 | The test its the same for all Adapters and look like this.
16 |
17 | ``` php
18 | set(md5($i), md5($i), 3600);
23 | }
24 | $timer->mark('10.000 set');
25 | for ($i = 1; $i <= 10000; $i++) {
26 | $cache->has(md5($i));
27 | }
28 | $timer->mark('10.000 has');
29 | for ($i = 1; $i <= 10000; $i++) {
30 | $cache->get(md5($i));
31 | }
32 | $timer->mark('10.000 get');
33 |
34 | ```
35 |
36 | if you want run the tests them execute.
37 |
38 | ``` sh
39 | php test/performance/AdapterName.php
40 | ```
--------------------------------------------------------------------------------
/src/Packer/NopPacker.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache\Packer;
18 |
19 | use Desarrolla2\Cache\Packer\PackerInterface;
20 |
21 | /**
22 | * Don't pack, just straight passthrough
23 | */
24 | class NopPacker implements PackerInterface
25 | {
26 | /**
27 | * Get cache type (might be used as file extension)
28 | *
29 | * @return string
30 | */
31 | public function getType()
32 | {
33 | return 'data';
34 | }
35 |
36 | /**
37 | * Pack the value
38 | *
39 | * @param mixed $value
40 | * @return mixed
41 | */
42 | public function pack($value)
43 | {
44 | return $value;
45 | }
46 |
47 | /**
48 | * Unpack the value
49 | *
50 | * @param mixed $packed
51 | * @return mixed
52 | */
53 | public function unpack($packed)
54 | {
55 | return $packed;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Option/TtlTrait.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Option;
17 |
18 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
19 |
20 | /**
21 | * TTL option
22 | */
23 | trait TtlTrait
24 | {
25 | /**
26 | * @var int|null
27 | */
28 | protected $ttl = null;
29 |
30 | /**
31 | * Set the maximum time to live (ttl)
32 | *
33 | * @param int|null $value Seconds or null to live forever
34 | * @throws InvalidArgumentException
35 | */
36 | protected function setTtlOption(?int $value): void
37 | {
38 | if (isset($value) && $value < 1) {
39 | throw new InvalidArgumentException('ttl cant be lower than 1');
40 | }
41 |
42 | $this->ttl = $value;
43 | }
44 |
45 | /**
46 | * Get the maximum time to live (ttl)
47 | *
48 | * @return int|null
49 | */
50 | protected function getTtlOption(): ?int
51 | {
52 | return $this->ttl;
53 | }
54 | }
--------------------------------------------------------------------------------
/tests/PredisTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Predis as PredisCache;
17 | use Predis\Client;
18 | use Predis\Connection\ConnectionException;
19 |
20 | /**
21 | * PredisTest
22 | */
23 | class PredisTest extends AbstractCacheTest
24 | {
25 | /**
26 | * @var Client
27 | */
28 | protected $client;
29 |
30 | public function createSimpleCache()
31 | {
32 | if (!class_exists('Predis\Client')) {
33 | $this->markTestSkipped('The predis library is not available');
34 | }
35 |
36 | try {
37 | $this->client = new Client(CACHE_TESTS_PREDIS_DSN, ['exceptions' => false]);
38 | $this->client->connect();
39 | } catch (ConnectionException $e) {
40 | $this->markTestSkipped($e->getMessage());
41 | }
42 |
43 | return new PredisCache($this->client);
44 | }
45 |
46 | public function tearDown(): void
47 | {
48 | parent::tearDown();
49 |
50 | if ($this->client) {
51 | $this->client->disconnect();
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/RedisTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Redis as RedisCache;
17 | use Redis as PhpRedis;
18 |
19 | /**
20 | * RedisTest
21 | */
22 | class RedisTest extends AbstractCacheTest
23 | {
24 | /**
25 | * @var PhpRedis
26 | */
27 | protected $client;
28 |
29 | public function createSimpleCache()
30 | {
31 | if (!\extension_loaded('redis')) {
32 | $this->markTestSkipped('Redis extension not available.');
33 | }
34 |
35 | $client = new PhpRedis();
36 |
37 | $success = @$client->connect(CACHE_TESTS_REDIS_HOST, CACHE_TESTS_REDIS_PORT);
38 | if (!$success) {
39 | $this->markTestSkipped('Cannot connect to Redis.');
40 | }
41 |
42 | $this->client = $client;
43 |
44 | return new RedisCache($this->client);
45 | }
46 |
47 | public function tearDown(): void
48 | {
49 | parent::tearDown();
50 |
51 | if ($this->client && $this->client->isConnected()) {
52 | $this->client->close();
53 | }
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/tests/MongoDBTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\MongoDB as MongoDBCache;
17 | use MongoDB\Client;
18 |
19 | /**
20 | * MongoDBTest
21 | */
22 | class MongoDBTest extends AbstractCacheTest
23 | {
24 | /**
25 | * @var Client
26 | */
27 | protected static $client;
28 |
29 | /**
30 | * Use one client per test, as the MongoDB extension leaves connections open
31 | */
32 | public static function setUpBeforeClass(): void
33 | {
34 | if (!extension_loaded('mongodb')) {
35 | return;
36 | }
37 |
38 | self::$client = new Client(CACHE_TESTS_MONGO_DSN);
39 | self::$client->listDatabases(); // Fail if unable to connect
40 | }
41 |
42 | public function createSimpleCache()
43 | {
44 | if (!isset(self::$client)) {
45 | $this->markTestSkipped('The mongodb extension is not available');
46 | }
47 |
48 | $collection = self::$client->selectCollection(CACHE_TESTS_MONGO_DATABASE, 'cache');
49 |
50 | return (new MongoDBCache($collection))
51 | ->withOption('initialize', false);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/CacheInterface.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | namespace Desarrolla2\Cache;
16 |
17 | use Psr\SimpleCache\CacheInterface as PsrCacheInterface;
18 | use Desarrolla2\Cache\Packer\PackerInterface;
19 | use Desarrolla2\Cache\KeyMaker\KeyMakerInterface;
20 |
21 | /**
22 | * CacheInterface
23 | */
24 | interface CacheInterface extends PsrCacheInterface
25 | {
26 | /**
27 | * Set option for cache
28 | *
29 | * @param string $key
30 | * @param mixed $value
31 | * @return static
32 | */
33 | public function withOption(string $key, $value);
34 |
35 | /**
36 | * Set multiple options for cache
37 | *
38 | * @param array $options
39 | * @return static
40 | */
41 | public function withOptions(array $options);
42 |
43 | /**
44 | * Get option for cache
45 | *
46 | * @param string $key
47 | * @return mixed
48 | */
49 | public function getOption($key);
50 |
51 | /**
52 | * Set the packer
53 | *
54 | * @param PackerInterface $packer
55 | * @return static
56 | */
57 | public function withPacker(PackerInterface $packer);
58 | }
59 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | ./tests
17 |
18 |
19 |
20 |
21 |
22 | ./src
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/File/BasicFilename.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\File;
17 |
18 | /**
19 | * Create a path for a key
20 | */
21 | class BasicFilename
22 | {
23 | /**
24 | * @var string
25 | */
26 | protected $format;
27 |
28 | /**
29 | * BasicFilename constructor.
30 | *
31 | * @param string $format
32 | */
33 | public function __construct(string $format)
34 | {
35 | $this->format = $format;
36 | }
37 |
38 | /**
39 | * Get the format
40 | *
41 | * @return string
42 | */
43 | public function getFormat(): string
44 | {
45 | return $this->format;
46 | }
47 |
48 | /**
49 | * Create the path for a key
50 | *
51 | * @param string $key
52 | * @return string
53 | */
54 | public function __invoke(string $key): string
55 | {
56 | return sprintf($this->format, $key ?: '*');
57 | }
58 |
59 | /**
60 | * Cast to string
61 | *
62 | * @return string
63 | */
64 | public function __toString(): string
65 | {
66 | return $this->getFormat();
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Option/InitializeTrait.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Option;
17 |
18 | /**
19 | * Auto initialize the cache
20 | */
21 | trait InitializeTrait
22 | {
23 | /**
24 | * Is cache initialized
25 | * @var bool|null
26 | */
27 | protected $initialized = false;
28 |
29 |
30 | /**
31 | * Enable/disable initialization
32 | *
33 | * @param bool $enabled
34 | */
35 | public function setInitializeOption(bool $enabled)
36 | {
37 | $this->initialized = $enabled ? (bool)$this->initialized : null;
38 | }
39 |
40 | /**
41 | * Should initialize
42 | *
43 | * @return bool
44 | */
45 | protected function getInitializeOption(): bool
46 | {
47 | return $this->initialized !== null;
48 | }
49 |
50 | /**
51 | * Mark as initialization required (if enabled)
52 | */
53 | protected function requireInitialization()
54 | {
55 | $this->initialized = isset($this->initialized) ? false : null;
56 | }
57 |
58 |
59 | /**
60 | * Initialize
61 | *
62 | * @return void
63 | */
64 | abstract protected function initialize(): void;
65 | }
66 |
--------------------------------------------------------------------------------
/docs/implementations/mysqli.md:
--------------------------------------------------------------------------------
1 | # Mysqli
2 |
3 | Cache to a [MySQL database](https://www.mysql.com/) using the
4 | [mysqli](http://php.net/manual/en/book.mysqli.php) PHP extension.
5 |
6 | You must pass a `mysqli` connection object to the constructor.
7 |
8 | ``` php
9 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Memcached as MemcachedCache;
17 | use Memcached;
18 |
19 | /**
20 | * MemcachedTest
21 | */
22 | class MemcachedTest extends AbstractCacheTest
23 | {
24 | protected $skippedTests = [
25 | 'testBasicUsageWithLongKey' => 'Only support keys up to 250 bytes'
26 | ];
27 |
28 | public function createSimpleCache()
29 | {
30 | if (!extension_loaded('memcached') || !class_exists('\Memcached')) {
31 | $this->markTestSkipped(
32 | 'The Memcached extension is not available.'
33 | );
34 | }
35 |
36 | // See https://github.com/php-memcached-dev/php-memcached/issues/509
37 | if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
38 | $this->markTestSkipped(
39 | 'The Memcached extension is does not work with PHP 8.1 yet.'
40 | );
41 | }
42 |
43 | list($host, $port) = explode(':', CACHE_TESTS_MEMCACHED_SERVER) + [1 => 11211];
44 |
45 | $adapter = new Memcached();
46 | $adapter->addServer($host, (int)$port);
47 |
48 | if (!$adapter->flush()) {
49 | $this->markTestSkipped("Unable to flush Memcached; not running?");
50 | }
51 |
52 | return new MemcachedCache($adapter);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Packer/JsonPacker.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache\Packer;
18 |
19 | use Desarrolla2\Cache\Packer\PackerInterface;
20 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
21 |
22 | /**
23 | * Pack value through serialization
24 | */
25 | class JsonPacker implements PackerInterface
26 | {
27 | /**
28 | * Get cache type (might be used as file extension)
29 | *
30 | * @return string
31 | */
32 | public function getType()
33 | {
34 | return 'json';
35 | }
36 |
37 | /**
38 | * Pack the value
39 | *
40 | * @param mixed $value
41 | * @return string
42 | */
43 | public function pack($value)
44 | {
45 | return json_encode($value);
46 | }
47 |
48 | /**
49 | * Unpack the value
50 | *
51 | * @param string $packed
52 | * @return mixed
53 | * @throws InvalidArgumentException
54 | */
55 | public function unpack($packed)
56 | {
57 | if (!is_string($packed)) {
58 | throw new InvalidArgumentException("packed value should be a string");
59 | }
60 |
61 | $ret = json_decode($packed);
62 |
63 | if (!isset($ret) && json_last_error()) {
64 | throw new \UnexpectedValueException("packed value is not a valid JSON string");
65 | }
66 |
67 | return $ret;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/docs/implementations/mongodb.md:
--------------------------------------------------------------------------------
1 | # Mongo
2 |
3 | Use it to store the cache in a Mongo database. Requires the mongodb extension
4 | and the [mongodb/mongodb](https://github.com/mongodb/mongo-php-library)
5 | library.
6 |
7 | You must pass a `MongoDB\Collection` object to the cache constructor.
8 |
9 | ``` php
10 | selectDatabase('mycache');
17 | $collection = $database->selectCollection('cache');
18 |
19 | $cache = new MongoCache($collection);
20 | ```
21 |
22 | MonoDB will always automatically create the database and collection if needed.
23 |
24 | ### Options
25 |
26 | | name | type | default | |
27 | | --------- | ---- | ------- | ------------------------------------- |
28 | | initialize | bool | true | Enable auto-initialize |
29 | | ttl | int | null | Maximum time to live in seconds |
30 | | prefix | string | "" | Key prefix |
31 |
32 | #### Initialize option
33 |
34 | If `initialize` is enabled, the cache implementation will automatically create
35 | a [ttl index](https://docs.mongodb.com/manual/core/index-ttl/). In production
36 | it's better to disable auto-initialization and create the ttl index explicitly
37 | when setting up the database. This prevents a `createIndex()` call on each
38 | request.
39 |
40 | ### Packer
41 |
42 | By default the [`MongoDBBinaryPacker`](../packers/mongodbbinary.md) is used. It
43 | serializes the data and stores it in a [Binary BSON variable](http://php.net/manual/en/class.mongodb-bson-binary.php).
44 | If the data is a UTF-8 string of simple array or stdClass object, it may be
45 | useful to use the [`NopPacker`](../packers/nop.md) instead.
46 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "desarrolla2/cache",
3 | "description": "Provides an cache interface for several adapters Apc, Apcu, File, Mongo, Memcache, Memcached, Mysql, Mongo, Redis is supported.",
4 | "keywords": [
5 | "cache",
6 | "simple-cache",
7 | "psr-16",
8 | "apc",
9 | "apcu",
10 | "file",
11 | "memcached",
12 | "memcache",
13 | "mysql",
14 | "mongo",
15 | "redis"
16 | ],
17 | "type": "library",
18 | "license": "MIT",
19 | "homepage": "https://github.com/desarrolla2/Cache/",
20 | "authors": [
21 | {
22 | "name": "Daniel González",
23 | "homepage": "http://desarrolla2.com/"
24 | },
25 | {
26 | "name": "Arnold Daniels",
27 | "homepage": "https://jasny.net/"
28 | }
29 | ],
30 | "provide": {
31 | "psr/simple-cache-implementation": "1.0"
32 | },
33 | "require": {
34 | "php": ">=7.2.0",
35 | "psr/simple-cache": "^1.0"
36 | },
37 | "require-dev": {
38 | "ext-apcu": "*",
39 | "ext-json": "*",
40 | "ext-mysqli": "*",
41 | "ext-memcached": "*",
42 | "ext-redis": "*",
43 | "predis/predis": "~1.0.0",
44 | "mongodb/mongodb": "^1.3",
45 | "cache/integration-tests": "dev-master",
46 | "phpunit/phpunit": "^8.3 || ^9.0",
47 | "phpstan/phpstan": "^0.12.29",
48 | "symfony/phpunit-bridge": "^5.2",
49 | "mikey179/vfsstream": "v1.6.10"
50 | },
51 | "autoload": {
52 | "psr-4": {
53 | "Desarrolla2\\Cache\\": "src/"
54 | }
55 | },
56 | "autoload-dev": {
57 | "psr-4": {
58 | "Desarrolla2\\Test\\Cache\\": "tests/"
59 | }
60 | },
61 | "scripts": {
62 | "test": [
63 | "phpstan analyse",
64 | "phpunit --colors=always",
65 | "phpcs -p src"
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Packer/SerializePacker.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache\Packer;
18 |
19 | use Desarrolla2\Cache\Packer\PackerInterface;
20 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
21 |
22 | /**
23 | * Pack value through serialization
24 | */
25 | class SerializePacker implements PackerInterface
26 | {
27 | /**
28 | * @var array
29 | */
30 | protected $options;
31 |
32 | /**
33 | * SerializePacker constructor
34 | *
35 | * @param array $options Any options to be provided to unserialize()
36 | */
37 | public function __construct(array $options = ['allowed_classes' => true])
38 | {
39 | $this->options = $options;
40 | }
41 |
42 | /**
43 | * Get cache type (might be used as file extension)
44 | *
45 | * @return string
46 | */
47 | public function getType()
48 | {
49 | return 'php.cache';
50 | }
51 |
52 | /**
53 | * Pack the value
54 | *
55 | * @param mixed $value
56 | * @return string
57 | */
58 | public function pack($value)
59 | {
60 | return serialize($value);
61 | }
62 |
63 | /**
64 | * Unpack the value
65 | *
66 | * @param string $packed
67 | * @return string
68 | * @throws \UnexpectedValueException if he value can't be unpacked
69 | */
70 | public function unpack($packed)
71 | {
72 | if (!is_string($packed)) {
73 | throw new InvalidArgumentException("packed value should be a string");
74 | }
75 |
76 | return unserialize($packed, $this->options);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/NotCache.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\AbstractCache;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\NopPacker;
22 |
23 | /**
24 | * Dummy cache handler
25 | */
26 | class NotCache extends AbstractCache
27 | {
28 | /**
29 | * Create the default packer for this cache implementation.
30 | *
31 | * @return PackerInterface
32 | */
33 | protected static function createDefaultPacker(): PackerInterface
34 | {
35 | return new NopPacker();
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function delete($key)
42 | {
43 | return true;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function get($key, $default = null)
50 | {
51 | return false;
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function getMultiple($keys, $default = null)
58 | {
59 | return false;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function has($key)
66 | {
67 | return false;
68 | }
69 |
70 | /**
71 | * {@inheritdoc}
72 | */
73 | public function set($key, $value, $ttl = null)
74 | {
75 | return false;
76 | }
77 |
78 | /**
79 | * {@inheritdoc}
80 | */
81 | public function setMultiple($values, $ttl = null)
82 | {
83 | return false;
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function clear()
90 | {
91 | return true;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Packer/MongoDBBinaryPacker.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | namespace Desarrolla2\Cache\Packer;
15 |
16 | use Desarrolla2\Cache\Packer\PackerInterface;
17 | use MongoDB\BSON\Binary;
18 |
19 | /**
20 | * Pack as BSON binary
21 | *
22 | * @todo Don't use serialize when packer chain is here.
23 | */
24 | class MongoDBBinaryPacker implements PackerInterface
25 | {
26 | /**
27 | * @var array
28 | */
29 | protected $options;
30 |
31 | /**
32 | * SerializePacker constructor
33 | *
34 | * @param array $options Any options to be provided to unserialize()
35 | */
36 | public function __construct(array $options = ['allowed_classes' => true])
37 | {
38 | $this->options = $options;
39 | }
40 |
41 | /**
42 | * Get cache type (might be used as file extension)
43 | *
44 | * @return string
45 | */
46 | public function getType()
47 | {
48 | return 'bson';
49 | }
50 |
51 | /**
52 | * Pack the value
53 | *
54 | * @param mixed $value
55 | * @return string
56 | */
57 | public function pack($value)
58 | {
59 | return new Binary(serialize($value), Binary::TYPE_GENERIC);
60 | }
61 |
62 | /**
63 | * Unpack the value
64 | *
65 | * @param string $packed
66 | * @return string
67 | * @throws \UnexpectedValueException if he value can't be unpacked
68 | */
69 | public function unpack($packed)
70 | {
71 | if (!$packed instanceof Binary) {
72 | throw new \InvalidArgumentException("packed value should be BSON binary");
73 | }
74 |
75 | return unserialize((string)$packed, $this->options);
76 | }
77 | }
--------------------------------------------------------------------------------
/tests/NotCacheTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\NotCache as NotCache;
17 | use PHPUnit\Framework\TestCase;
18 |
19 | /**
20 | * NotCacheTest
21 | */
22 | class NotCacheTest extends TestCase
23 | {
24 | /**
25 | * @var \Desarrolla2\Cache\NotCache
26 | */
27 | protected $cache;
28 |
29 | public function setUp(): void
30 | {
31 | $this->cache = new NotCache();
32 | }
33 |
34 | /**
35 | * @return array
36 | */
37 | public function dataProvider()
38 | {
39 | return array(
40 | array(),
41 | );
42 | }
43 |
44 | /**
45 | * @dataProvider dataProvider
46 | */
47 | public function testHas()
48 | {
49 | $this->cache->set('key', 'value');
50 | $this->assertFalse($this->cache->has('key'));
51 | }
52 |
53 | /**
54 | * @dataProvider dataProvider
55 | */
56 | public function testGet()
57 | {
58 | $this->cache->set('key', 'value');
59 | $this->assertFalse($this->cache->get('key', false));
60 | }
61 |
62 | /**
63 | * @dataProvider dataProvider
64 | */
65 | public function testSet()
66 | {
67 | $this->assertFalse($this->cache->set('key', 'value'));
68 | }
69 |
70 | /**
71 | * @dataProvider dataProvider
72 | */
73 | public function testDelete()
74 | {
75 | $this->assertTrue($this->cache->delete('key'));
76 | }
77 |
78 | /**
79 | * @dataProvider dataProvider
80 | */
81 | public function testWithOption()
82 | {
83 | $cache = $this->cache->withOption('ttl', 3600);
84 | $this->assertSame(3600, $cache->getOption('ttl'));
85 |
86 | $this->assertNotSame($this->cache, $cache);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Apcu.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Exception\CacheException;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\NopPacker;
22 |
23 | /**
24 | * Apcu
25 | */
26 | class Apcu extends AbstractCache
27 | {
28 | /**
29 | * Create the default packer for this cache implementation
30 | *
31 | * @return PackerInterface
32 | */
33 | protected static function createDefaultPacker(): PackerInterface
34 | {
35 | return new NopPacker();
36 | }
37 |
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function set($key, $value, $ttl = null)
43 | {
44 | $ttlSeconds = $this->ttlToSeconds($ttl);
45 |
46 | if (isset($ttlSeconds) && $ttlSeconds <= 0) {
47 | return $this->delete($key);
48 | }
49 |
50 | return apcu_store($this->keyToId($key), $this->pack($value), $ttlSeconds ?? 0);
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function get($key, $default = null)
57 | {
58 | $packed = apcu_fetch($this->keyToId($key), $success);
59 |
60 | return $success ? $this->unpack($packed) : $default;
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function has($key)
67 | {
68 | return apcu_exists($this->keyToId($key));
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | */
74 | public function delete($key)
75 | {
76 | $id = $this->keyToId($key);
77 |
78 | return apcu_delete($id) || !apcu_exists($id);
79 | }
80 |
81 | /**
82 | * {@inheritdoc}
83 | */
84 | public function clear()
85 | {
86 | return apcu_clear_cache();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Packer/PackingTrait.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Packer;
17 |
18 | /**
19 | * Support packing for Caching adapter
20 | */
21 | trait PackingTrait
22 | {
23 | /**
24 | * @var PackerInterface
25 | */
26 | protected $packer;
27 |
28 |
29 | /**
30 | * Create the default packer for this cache implementation
31 | *
32 | * @return PackerInterface
33 | */
34 | abstract protected static function createDefaultPacker(): PackerInterface;
35 |
36 | /**
37 | * Set a packer to pack (serialialize) and unpack (unserialize) the data.
38 | *
39 | * @param PackerInterface $packer
40 | * @return static
41 | */
42 | public function withPacker(PackerInterface $packer)
43 | {
44 | $cache = $this->cloneSelf();
45 | $cache->packer = $packer;
46 |
47 | return $cache;
48 | }
49 |
50 | /**
51 | * Get the packer
52 | *
53 | * @return PackerInterface
54 | */
55 | protected function getPacker(): PackerInterface
56 | {
57 | if (!isset($this->packer)) {
58 | $this->packer = static::createDefaultPacker();
59 | }
60 |
61 | return $this->packer;
62 | }
63 |
64 | /**
65 | * Pack the value
66 | *
67 | * @param mixed $value
68 | * @return string|mixed
69 | */
70 | protected function pack($value)
71 | {
72 | return $this->getPacker()->pack($value);
73 | }
74 |
75 | /**
76 | * Unpack the data to retrieve the value
77 | *
78 | * @param string|mixed $packed
79 | * @return mixed
80 | * @throws \UnexpectedValueException
81 | */
82 | protected function unpack($packed)
83 | {
84 | return $this->getPacker()->unpack($packed);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | run:
11 | runs-on: ubuntu-latest
12 | services:
13 | mysql:
14 | image: mysql:5.7
15 | ports:
16 | - 3306:3306
17 | env:
18 | MYSQL_ALLOW_EMPTY_PASSWORD: yes
19 | redis:
20 | image: redis:6.0
21 | ports:
22 | - 6379:6379
23 | mongo:
24 | image: mongo:4.2-bionic
25 | ports:
26 | - 27017:27017
27 | memcached:
28 | image: memcached:1.6
29 | ports:
30 | - 11211:11211
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | include:
36 | - php: 7.2
37 | composer: '--prefer-lowest'
38 | desc: "Lowest versions"
39 | - php: 7.4
40 | composer: '--prefer-lowest'
41 | desc: "Lowest versions"
42 | - php: 7.2
43 | - php: 7.3
44 | - php: 7.4
45 | coverage: '--coverage-clover /tmp/clover.xml'
46 | - php: 8.0
47 | - php: 8.1
48 | name: PHP ${{ matrix.php }} ${{ matrix.desc }}
49 |
50 | steps:
51 | - uses: actions/checkout@v2
52 |
53 | - name: Setup PHP
54 | uses: shivammathur/setup-php@v2
55 | with:
56 | php-version: ${{ matrix.php }}
57 | coverage: xdebug
58 | extensions: apcu, mongodb, memcached
59 | ini-values: apc.enable_cli=1,mysqli.default_host=127.0.0.1,mysqli.default_port=3306,mysqli.default_user=root
60 |
61 | - name: Validate composer.json and composer.lock
62 | run: composer validate
63 |
64 | - name: Install dependencies
65 | run: composer update --prefer-dist --no-progress ${{ matrix.composer }}
66 |
67 | - name: Run PHPUnit
68 | run: vendor/bin/phpunit ${{ matrix.coverage }}
69 |
70 | - name: Upload coverage to Scrutinizer
71 | if: ${{ matrix.coverage }}
72 | run: >
73 | wget https://scrutinizer-ci.com/ocular.phar -O "/tmp/ocular.phar" &&
74 | php "/tmp/ocular.phar" code-coverage:upload --format=php-clover /tmp/clover.xml
75 |
--------------------------------------------------------------------------------
/tests/MysqliTest.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 |
13 | namespace Desarrolla2\Test\Cache;
14 |
15 | use Desarrolla2\Cache\Mysqli as MysqliCache;
16 |
17 | /**
18 | * MysqliTest
19 | */
20 | class MysqliTest extends AbstractCacheTest
21 | {
22 | /**
23 | * @var \mysqli
24 | */
25 | protected static $mysqli;
26 |
27 | protected $skippedTests = [
28 | 'testBasicUsageWithLongKey' => 'Only support keys up to 255 bytes'
29 | ];
30 |
31 | public static function setUpBeforeClass(): void
32 | {
33 | if (class_exists('mysqli')) {
34 | static::$mysqli = new \mysqli(
35 | ini_get('mysqli.default_host') ?: 'localhost',
36 | ini_get('mysqli.default_user') ?: 'root'
37 | );
38 | }
39 | }
40 |
41 | public function init(): void
42 | {
43 | if (!class_exists('mysqli')) {
44 | $this->markTestSkipped("mysqli extension not loaded");
45 | }
46 |
47 | try {
48 | static::$mysqli->query('CREATE DATABASE IF NOT EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`');
49 | static::$mysqli->select_db(CACHE_TESTS_MYSQLI_DATABASE);
50 |
51 | static::$mysqli->query("CREATE TABLE IF NOT EXISTS `cache` "
52 | ."( `key` VARCHAR(255), `value` BLOB, `ttl` INT UNSIGNED, PRIMARY KEY (`key`) )");
53 | } catch (\Exception $e) {
54 | $this->markTestSkipped("skipping mysqli test; " . $e->getMessage());
55 | }
56 |
57 | if (static::$mysqli->error) {
58 | $this->markTestSkipped(static::$mysqli->error);
59 | }
60 | }
61 |
62 | public function createSimpleCache()
63 | {
64 | $this->init();
65 |
66 | return (new MysqliCache(static::$mysqli))
67 | ->withOption('initialize', false);
68 | }
69 |
70 | public static function tearDownAfterClass(): void
71 | {
72 | static::$mysqli->query('DROP DATABASE IF EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`');
73 | static::$mysqli->close();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Option/FilenameTrait.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\Option;
17 |
18 | use TypeError;
19 | use Desarrolla2\Cache\File\BasicFilename;
20 |
21 | /**
22 | * Use filename generator
23 | */
24 | trait FilenameTrait
25 | {
26 | /**
27 | * @var callable
28 | */
29 | protected $filename;
30 |
31 |
32 | /**
33 | * Filename format or callable.
34 | * The filename format will be applied using sprintf, replacing `%s` with the key.
35 | *
36 | * @param string|callable $filename
37 | * @return void
38 | */
39 | protected function setFilenameOption($filename): void
40 | {
41 | if (is_string($filename)) {
42 | $filename = new BasicFilename($filename);
43 | }
44 |
45 | if (!is_callable($filename)) {
46 | throw new TypeError("Filename should be a string or callable");
47 | }
48 |
49 | $this->filename = $filename;
50 | }
51 |
52 | /**
53 | * Get the filename callable
54 | *
55 | * @return callable
56 | */
57 | protected function getFilenameOption(): callable
58 | {
59 | if (!isset($this->filename)) {
60 | $this->filename = new BasicFilename('%s.' . $this->getPacker()->getType());
61 | }
62 |
63 | return $this->filename;
64 | }
65 |
66 | /**
67 | * Create a filename based on the key
68 | *
69 | * @param string|mixed $key
70 | * @return string
71 | */
72 | protected function getFilename($key): string
73 | {
74 | $id = $this->keyToId($key);
75 | $generator = $this->getFilenameOption();
76 |
77 | return $this->cacheDir . DIRECTORY_SEPARATOR . $generator($id);
78 | }
79 |
80 | /**
81 | * Get a wildcard for all files
82 | *
83 | * @return string
84 | */
85 | protected function getWildcard(): string
86 | {
87 | $generator = $this->getFilenameOption();
88 |
89 | return $this->cacheDir . DIRECTORY_SEPARATOR . $generator('');
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/AbstractCacheTest.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 |
13 | namespace Desarrolla2\Test\Cache;
14 |
15 | use Cache\IntegrationTests\SimpleCacheTest;
16 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
17 |
18 | /**
19 | * AbstractCacheTest
20 | */
21 | abstract class AbstractCacheTest extends SimpleCacheTest
22 | {
23 | /**
24 | * @return array
25 | */
26 | public function dataProviderForOptions()
27 | {
28 | return [
29 | ['ttl', 100],
30 | ['prefix', 'test']
31 | ];
32 | }
33 |
34 | /**
35 | * @dataProvider dataProviderForOptions
36 | *
37 | * @param string $key
38 | * @param mixed $value
39 | */
40 | public function testWithOption($key, $value)
41 | {
42 | $cache = $this->cache->withOption($key, $value);
43 | $this->assertEquals($value, $cache->getOption($key));
44 |
45 | // Check immutability
46 | $this->assertNotSame($this->cache, $cache);
47 | $this->assertNotEquals($value, $this->cache->getOption($key));
48 | }
49 |
50 | public function testWithOptions()
51 | {
52 | $data = $this->dataProviderForOptions();
53 | $options = array_combine(array_column($data, 0), array_column($data, 1));
54 |
55 | $cache = $this->cache->withOptions($options);
56 |
57 | foreach ($options as $key => $value) {
58 | $this->assertEquals($value, $cache->getOption($key));
59 | }
60 |
61 | // Check immutability
62 | $this->assertNotSame($this->cache, $cache);
63 |
64 | foreach ($options as $key => $value) {
65 | $this->assertNotEquals($value, $this->cache->getOption($key));
66 | }
67 | }
68 |
69 |
70 | /**
71 | * @return array
72 | */
73 | public function dataProviderForOptionsException()
74 | {
75 | return [
76 | ['ttl', 0, InvalidArgumentException::class],
77 | ['foo', 'bar', InvalidArgumentException::class]
78 | ];
79 | }
80 |
81 | /**
82 | * @dataProvider dataProviderForOptionsException
83 | *
84 | * @param string $key
85 | * @param mixed $value
86 | * @param string $expectedException
87 | */
88 | public function testWithOptionException($key, $value, $expectedException)
89 | {
90 | $this->expectException($expectedException);
91 | $this->createSimpleCache()->withOption($key, $value);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/docs/implementations/phpfile.md:
--------------------------------------------------------------------------------
1 | # PhpFile
2 |
3 | Save the cache as PHP script to on the filesystem using
4 | [`var_export`](http://php.net/manual/en/function.var-export.php) when storing
5 | cache and [`include`](http://php.net/manual/en/function.include.php) when
6 | loading cache.
7 |
8 | The implementation leverages the PHP engine’s in-memory file caching (opcache)
9 | to cache application data in addition to code. This method is particularly fast
10 | in PHP7.2+ due to opcode cache optimizations.
11 |
12 | PHP file caching should primarily be used for arrays and objects. There is no
13 | performance benefit over APCu for storing strings.
14 |
15 | [read more][]
16 |
17 | ``` php
18 | use Desarrolla2\Cache\PhpFile as PhpFileCache;
19 |
20 | $cache = new PhpFileCache();
21 | ```
22 |
23 | ### Options
24 |
25 | | name | type | default | |
26 | | --------- | ------------------ | -------------- | ------------------------------------- |
27 | | ttl | int | null | Maximum time to live in seconds |
28 | | prefix | string | "" | Key prefix |
29 | | filename | string or callable | "%s.php" | Filename as sprintf format |
30 |
31 | #### Filename option
32 |
33 | The `filename` will be parsed using `sprintf` where '%s' is substituted with
34 | the key.
35 |
36 | Instead of a string, `filename` may also be set to a callable, like a callable
37 | object or closure. In that case the callable will be called to create a
38 | filename as
39 |
40 | $filename = $callable($key);
41 |
42 | ##### BasicFilename
43 |
44 | The library comes with invokable object as callable for the filename. The
45 | `BasicFilename` object works as described above.
46 |
47 | ##### TrieFilename
48 |
49 | The `TrieFilename` object will create a prefix tree directory structure. This
50 | is useful where a lot of cache files would cause to many files in a directory.
51 |
52 | Specify the `sprintf` format and the directory level to the constructor when
53 | creating a `TrieFilename` object.
54 |
55 | ``` php
56 | use Desarrolla2\Cache\File as FileCache;
57 | use Desarrolla2\Cache\File\TrieFilename;
58 |
59 | $callback = new TrieFilename('%s.php', 2);
60 |
61 | $cache = (new FileCache(sys_get_temp_dir() . '/cache'))
62 | ->withOption('filename', $callback);
63 | ```
64 |
65 | In this case, adding an item with key `foobar` would be create a file at
66 |
67 | /tmp/cache/f/fo/foobar.php
68 |
69 | ### Packer
70 |
71 | By default the [`NopPacker`](../packers/nop.md) is used. Other packers should
72 | not be used.
73 |
74 | [read more]: https://medium.com/@dylanwenzlau/500x-faster-caching-than-redis-memcache-apc-in-php-hhvm-dcd26e8447ad
75 |
--------------------------------------------------------------------------------
/src/File/TrieFilename.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache\File;
17 |
18 | /**
19 | * Create a path for a key as prefix tree directory structure.
20 | *
21 | * @see https://en.wikipedia.org/wiki/Trie
22 | */
23 | class TrieFilename
24 | {
25 | /**
26 | * @var string
27 | */
28 | protected $format;
29 |
30 | /**
31 | * @var int
32 | */
33 | protected $levels;
34 |
35 | /**
36 | * @var bool
37 | */
38 | protected $hash;
39 |
40 |
41 | /**
42 | * TrieFilename constructor.
43 | *
44 | * @param string $format
45 | * @param int $levels The depth of the structure
46 | * @param bool $hash MD5 hash the key to get a better spread
47 | */
48 | public function __construct(string $format, int $levels = 1, bool $hash = false)
49 | {
50 | $this->format = $format;
51 | $this->levels = $levels;
52 | $this->hash = $hash;
53 | }
54 |
55 | /**
56 | * Get the format
57 | *
58 | * @return string
59 | */
60 | public function getFormat(): string
61 | {
62 | return $this->format;
63 | }
64 |
65 | /**
66 | * Get the depth of the structure
67 | *
68 | * @return int
69 | */
70 | public function getLevels(): int
71 | {
72 | return $this->levels;
73 | }
74 |
75 | /**
76 | * Will the key be hashed to create the trie.
77 | *
78 | * @return bool
79 | */
80 | public function isHashed(): bool
81 | {
82 | return $this->hash;
83 | }
84 |
85 |
86 | /**
87 | * Create the path for a key
88 | *
89 | * @param string $key
90 | * @return string
91 | */
92 | public function __invoke(string $key): string
93 | {
94 | if (empty($key)) {
95 | return $this->wildcardPath();
96 | }
97 |
98 | $dirname = $this->hash ? base_convert(md5($key), 16, 36) : $key;
99 | $filename = sprintf($this->format, $key);
100 |
101 | $path = '';
102 |
103 | for ($length = 1; $length <= $this->levels; $length++) {
104 | $path .= substr($dirname, 0, $length) . DIRECTORY_SEPARATOR;
105 | }
106 |
107 | return $path . $filename;
108 | }
109 |
110 | /**
111 | * Get a path for all files (using glob)
112 | *
113 | * @return string
114 | */
115 | protected function wildcardPath(): string
116 | {
117 | $filename = sprintf($this->format, '*');
118 |
119 | return str_repeat('*' . DIRECTORY_SEPARATOR, $this->levels) . $filename;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/PhpFile.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\AbstractFile;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\SerializePacker;
22 | use Desarrolla2\Cache\File\BasicFilename;
23 |
24 | /**
25 | * Cache file as PHP script.
26 | */
27 | class PhpFile extends AbstractFile
28 | {
29 | /**
30 | * Create the default packer for this cache implementation.
31 | *
32 | * @return PackerInterface
33 | */
34 | protected static function createDefaultPacker(): PackerInterface
35 | {
36 | return new SerializePacker();
37 | }
38 |
39 | /**
40 | * Get the filename callable
41 | *
42 | * @return callable
43 | */
44 | protected function getFilenameOption(): callable
45 | {
46 | if (!isset($this->filename)) {
47 | $this->filename = new BasicFilename('%s.php');
48 | }
49 |
50 | return $this->filename;
51 | }
52 |
53 | /**
54 | * Create a PHP script returning the cached value
55 | *
56 | * @param mixed $value
57 | * @param int|null $ttl
58 | * @return string
59 | */
60 | public function createScript($value, ?int $ttl): string
61 | {
62 | $macro = var_export($value, true);
63 |
64 | if (strpos($macro, 'stdClass::__set_state') !== false) {
65 | $macro = preg_replace_callback("/('([^'\\\\]++|''\\.)')|stdClass::__set_state/", $macro, function($match) {
66 | return empty($match[1]) ? '(object)' : $match[1];
67 | });
68 | }
69 |
70 | return $ttl !== null
71 | ? "getFilename($key);
81 |
82 | if (!file_exists($cacheFile)) {
83 | return $default;
84 | }
85 |
86 | $packed = include $cacheFile;
87 |
88 | return $packed === false ? $default : $this->unpack($packed);
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function has($key)
95 | {
96 | return $this->get($key) !== null;
97 | }
98 |
99 | /**
100 | * {@inheritdoc}
101 | */
102 | public function set($key, $value, $ttl = null)
103 | {
104 | $cacheFile = $this->getFilename($key);
105 |
106 | $packed = $this->pack($value);
107 | $script = $this->createScript($packed, $this->ttlToTimestamp($ttl));
108 |
109 | return $this->writeFile($cacheFile, $script);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/docs/implementations/file.md:
--------------------------------------------------------------------------------
1 | # File
2 |
3 | Save the cache as file to on the filesystem.
4 |
5 | You must pass a cache directory to the constructor.
6 |
7 | ``` php
8 | use Desarrolla2\Cache\File as FileCache;
9 |
10 | $cache = new FileCache(sys_get_temp_dir() . '/cache');
11 | ```
12 |
13 | ### Options
14 |
15 | | name | type | default | |
16 | | ------------ | --------------------------------- | -------------- | ------------------------------------- |
17 | | ttl | int | null | Maximum time to live in seconds |
18 | | ttl-strategy | string ('embed', 'file', 'mtime') | "embed" | Strategy to store the TTL |
19 | | prefix | string | "" | Key prefix |
20 | | filename | string or callable | "%s.php.cache" | Filename as sprintf format |
21 |
22 | #### TTL strategy option
23 |
24 | The ttl strategy determines how the TTL is stored. Typical filesystems don't
25 | allow custom file properties, so we'll have to use one of these strategies:
26 |
27 | | strategy | |
28 | | -------- | ----------------------------------------------- |
29 | | embed | Embed the TTL as first line of the file |
30 | | file | Create a TTL file in addition to the cache file |
31 | | mtime | Use [mtime][] + max ttl |
32 |
33 | The 'mtime' strategy is not PSR-16 compliant, as the TTL passed to the `set()`
34 | method is ignored. Only the `ttl` option for is used on `get()` and `has()`.
35 |
36 | [mtime]: https://www.unixtutorial.org/2008/04/atime-ctime-mtime-in-unix-filesystems/
37 |
38 | #### Filename option
39 |
40 | The `filename` will be parsed using `sprintf` where '%s' is substituted with
41 | the key. The default extension is automatically determined based on the
42 | packer.
43 |
44 | Instead of a string, `filename` may also be set to a callable, like a callable
45 | object or closure. In that case the callable will be called to create a
46 | filename as
47 |
48 | $filename = $callable($key);
49 |
50 | ##### BasicFilename
51 |
52 | The library comes with invokable object as callable for the filename. The
53 | `BasicFilename` object works as described above.
54 |
55 | ##### TrieFilename
56 |
57 | The `TrieFilename` object will create a prefix tree directory structure. This
58 | is useful where a lot of cache files would cause to many files in a directory.
59 |
60 | Specify the `sprintf` format and the directory level to the constructor when
61 | creating a `TrieFilename` object.
62 |
63 | ``` php
64 | use Desarrolla2\Cache\File as FileCache;
65 | use Desarrolla2\Cache\File\TrieFilename;
66 |
67 | $callback = new TrieFilename('%s.php.cache', 2);
68 |
69 | $cache = (new FileCache(sys_get_temp_dir() . '/cache'))
70 | ->withOption('filename', $callback);
71 | ```
72 |
73 | In this case, adding an item with key `foobar` would be create a file at
74 |
75 | /tmp/cache/f/fo/foobar.php.cache
76 |
77 | ### Packer
78 |
79 | By default the [`SerializePacker`](../packers/serialize.md) is used. The
80 | [`NopPacker`](../packers/nop.md) can be used if the values are strings.
81 | Other packers, like the [`JsonPacker`](../packers/json.md) are also
82 | useful with file cache.
83 |
--------------------------------------------------------------------------------
/src/Memory.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Packer\PackerInterface;
20 | use Desarrolla2\Cache\Packer\SerializePacker;
21 |
22 | /**
23 | * Memory
24 | */
25 | class Memory extends AbstractCache
26 | {
27 | /**
28 | * Limit the amount of entries
29 | * @var int
30 | */
31 | protected $limit = PHP_INT_MAX;
32 |
33 |
34 | /**
35 | * @var array
36 | */
37 | protected $cache = [];
38 |
39 | /**
40 | * @var array
41 | */
42 | protected $cacheTtl = [];
43 |
44 |
45 | /**
46 | * Create the default packer for this cache implementation.
47 | * {@internal NopPacker might fail PSR-16, as cached objects would change}
48 | *
49 | * @return PackerInterface
50 | */
51 | protected static function createDefaultPacker(): PackerInterface
52 | {
53 | return new SerializePacker();
54 | }
55 |
56 | /**
57 | * Make a clone of this object.
58 | * Set by cache reference, thus using the same pool.
59 | *
60 | * @return static
61 | */
62 | protected function cloneSelf(): AbstractCache
63 | {
64 | $clone = clone $this;
65 |
66 | $clone->cache =& $this->cache;
67 | $clone->cacheTtl =& $this->cacheTtl;
68 |
69 | return $clone;
70 | }
71 |
72 | /**
73 | * Set the max number of items
74 | *
75 | * @param int $limit
76 | */
77 | protected function setLimitOption($limit)
78 | {
79 | $this->limit = (int)$limit ?: PHP_INT_MAX;
80 | }
81 |
82 | /**
83 | * Get the max number of items
84 | *
85 | * @return int
86 | */
87 | protected function getLimitOption()
88 | {
89 | return $this->limit;
90 | }
91 |
92 |
93 | /**
94 | * {@inheritdoc}
95 | */
96 | public function get($key, $default = null)
97 | {
98 | if (!$this->has($key)) {
99 | return $default;
100 | }
101 |
102 | $id = $this->keyToId($key);
103 |
104 | return $this->unpack($this->cache[$id]);
105 | }
106 |
107 | /**
108 | * {@inheritdoc}
109 | */
110 | public function has($key)
111 | {
112 | $id = $this->keyToId($key);
113 |
114 | if (!isset($this->cacheTtl[$id])) {
115 | return false;
116 | }
117 |
118 | if ($this->cacheTtl[$id] <= time()) {
119 | unset($this->cache[$id], $this->cacheTtl[$id]);
120 | return false;
121 | }
122 |
123 | return true;
124 | }
125 |
126 | /**
127 | * {@inheritdoc}
128 | */
129 | public function set($key, $value, $ttl = null)
130 | {
131 | if (count($this->cache) >= $this->limit) {
132 | $deleteKey = key($this->cache);
133 | unset($this->cache[$deleteKey], $this->cacheTtl[$deleteKey]);
134 | }
135 |
136 | $id = $this->keyToId($key);
137 |
138 | $this->cache[$id] = $this->pack($value);
139 | $this->cacheTtl[$id] = $this->ttlToTimestamp($ttl) ?? PHP_INT_MAX;
140 |
141 | return true;
142 | }
143 |
144 | /**
145 | * {@inheritdoc}
146 | */
147 | public function delete($key)
148 | {
149 | $id = $this->keyToId($key);
150 | unset($this->cache[$id], $this->cacheTtl[$id]);
151 |
152 | return true;
153 | }
154 |
155 | /**
156 | * {@inheritdoc}
157 | */
158 | public function clear()
159 | {
160 | $this->cache = [];
161 | $this->cacheTtl = [];
162 |
163 | return true;
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/Chain.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | namespace Desarrolla2\Cache;
15 |
16 | use Desarrolla2\Cache\Packer\NopPacker;
17 | use Desarrolla2\Cache\Packer\PackerInterface;
18 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
19 |
20 | /**
21 | * Use multiple cache adapters.
22 | */
23 | class Chain extends AbstractCache
24 | {
25 | /**
26 | * @var CacheInterface[]
27 | */
28 | protected $adapters;
29 |
30 | /**
31 | * Create the default packer for this cache implementation
32 | *
33 | * @return PackerInterface
34 | */
35 | protected static function createDefaultPacker(): PackerInterface
36 | {
37 | return new NopPacker();
38 | }
39 |
40 |
41 | /**
42 | * Chain constructor.
43 | *
44 | * @param CacheInterface[] $adapters Fastest to slowest
45 | */
46 | public function __construct(array $adapters)
47 | {
48 | foreach ($adapters as $adapter) {
49 | if (!$adapter instanceof CacheInterface) {
50 | throw new InvalidArgumentException("All adapters should be a cache implementation");
51 | }
52 | }
53 |
54 | $this->adapters = $adapters;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function set($key, $value, $ttl = null)
61 | {
62 | $success = true;
63 |
64 | foreach ($this->adapters as $adapter) {
65 | $success = $adapter->set($key, $value, $ttl) && $success;
66 | }
67 |
68 | return $success;
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | */
74 | public function setMultiple($values, $ttl = null)
75 | {
76 | $success = true;
77 |
78 | foreach ($this->adapters as $adapter) {
79 | $success = $adapter->setMultiple($values, $ttl) && $success;
80 | }
81 |
82 | return $success;
83 | }
84 |
85 | /**
86 | * {@inheritdoc}
87 | */
88 | public function get($key, $default = null)
89 | {
90 | foreach ($this->adapters as $adapter) {
91 | $result = $adapter->get($key); // Not using $default as we want to get null if the adapter doesn't have it
92 |
93 | if (isset($result)) {
94 | return $result;
95 | }
96 | }
97 |
98 | return $default;
99 | }
100 |
101 | /**
102 | * {@inheritdoc}
103 | */
104 | public function getMultiple($keys, $default = null)
105 | {
106 | $this->assertIterable($keys, 'keys are not iterable');
107 |
108 | $missing = [];
109 | $values = [];
110 |
111 | foreach ($keys as $key) {
112 | $this->assertKey($key);
113 |
114 | $missing[] = $key;
115 | $values[$key] = $default;
116 | }
117 |
118 | foreach ($this->adapters as $adapter) {
119 | if (empty($missing)) {
120 | break;
121 | }
122 |
123 | $found = [];
124 | foreach ($adapter->getMultiple($missing) as $key => $value) {
125 | if (isset($value)) {
126 | $found[$key] = $value;
127 | }
128 | }
129 |
130 | $values = array_merge($values, $found);
131 | $missing = array_values(array_diff($missing, array_keys($found)));
132 | }
133 |
134 | return $values;
135 | }
136 |
137 | /**
138 | * {@inheritdoc}
139 | */
140 | public function has($key)
141 | {
142 | foreach ($this->adapters as $adapter) {
143 | if ($adapter->has($key)) {
144 | return true;
145 | }
146 | }
147 |
148 | return false;
149 | }
150 |
151 | /**
152 | * {@inheritdoc}
153 | */
154 | public function delete($key)
155 | {
156 | $success = true;
157 |
158 | foreach ($this->adapters as $adapter) {
159 | $success = $adapter->delete($key) && $success;
160 | }
161 |
162 | return $success;
163 | }
164 |
165 | /**
166 | * {@inheritdoc}
167 | */
168 | public function deleteMultiple($keys)
169 | {
170 | $success = true;
171 |
172 | foreach ($this->adapters as $adapter) {
173 | $success = $adapter->deleteMultiple($keys) && $success;
174 | }
175 |
176 | return $success;
177 | }
178 |
179 | /**
180 | * {@inheritdoc}
181 | */
182 | public function clear()
183 | {
184 | $success = true;
185 |
186 | foreach ($this->adapters as $adapter) {
187 | $success = $adapter->clear() && $success;
188 | }
189 |
190 | return $success;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/File.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
20 | use Desarrolla2\Cache\Exception\UnexpectedValueException;
21 | use Desarrolla2\Cache\Packer\PackerInterface;
22 | use Desarrolla2\Cache\Packer\SerializePacker;
23 |
24 | /**
25 | * Cache file.
26 | */
27 | class File extends AbstractFile
28 | {
29 | /**
30 | * @var string 'embed', 'file', 'mtime'
31 | */
32 | protected $ttlStrategy = 'embed';
33 |
34 | /**
35 | * Create the default packer for this cache implementation
36 | *
37 | * @return PackerInterface
38 | */
39 | protected static function createDefaultPacker(): PackerInterface
40 | {
41 | return new SerializePacker();
42 | }
43 |
44 | /**
45 | * Set TTL strategy
46 | *
47 | * @param string $strategy
48 | */
49 | protected function setTtlStrategyOption($strategy)
50 | {
51 | if (!in_array($strategy, ['embed', 'file', 'mtime'])) {
52 | throw new InvalidArgumentException("Unknown strategy '$strategy', should be 'embed', 'file' or 'mtime'");
53 | }
54 |
55 | $this->ttlStrategy = $strategy;
56 | }
57 |
58 | /**
59 | * Get TTL strategy
60 | *
61 | * @return string
62 | */
63 | protected function getTtlStrategyOption(): string
64 | {
65 | return $this->ttlStrategy;
66 | }
67 |
68 |
69 | /**
70 | * Get the TTL using one of the strategies
71 | *
72 | * @param string $cacheFile
73 | * @return int
74 | */
75 | protected function getTtl(string $cacheFile)
76 | {
77 | switch ($this->ttlStrategy) {
78 | case 'embed':
79 | return (int)$this->readLine($cacheFile);
80 | case 'file':
81 | return file_exists("$cacheFile.ttl")
82 | ? (int)file_get_contents("$cacheFile.ttl")
83 | : PHP_INT_MAX;
84 | case 'mtime':
85 | return $this->ttl > 0 ? filemtime($cacheFile) + $this->ttl : PHP_INT_MAX;
86 | }
87 |
88 | throw new \InvalidArgumentException("Invalid TTL strategy '{$this->ttlStrategy}'");
89 | }
90 |
91 | /**
92 | * Set the TTL using one of the strategies
93 | *
94 | * @param int|null $expiration
95 | * @param string $contents
96 | * @param string $cacheFile
97 | * @return string The (modified) contents
98 | */
99 | protected function setTtl($expiration, $contents, $cacheFile)
100 | {
101 | switch ($this->ttlStrategy) {
102 | case 'embed':
103 | $contents = ($expiration ?? PHP_INT_MAX) . "\n" . $contents;
104 | break;
105 | case 'file':
106 | if ($expiration !== null) {
107 | file_put_contents("$cacheFile.ttl", $expiration);
108 | }
109 | break;
110 | case 'mtime':
111 | // nothing
112 | break;
113 | }
114 |
115 | return $contents;
116 | }
117 |
118 |
119 | /**
120 | * {@inheritdoc}
121 | */
122 | public function get($key, $default = null)
123 | {
124 | $cacheFile = $this->getFilename($key);
125 |
126 | if (!file_exists($cacheFile)) {
127 | return $default;
128 | }
129 |
130 | if ($this->ttlStrategy === 'embed') {
131 | [$ttl, $packed] = explode("\n", $this->readFile($cacheFile), 2);
132 | } else {
133 | $ttl = $this->getTtl($cacheFile);
134 | }
135 |
136 | if ((int)$ttl <= time()) {
137 | $this->deleteFile($cacheFile);
138 | return $default;
139 | }
140 |
141 | if (!isset($packed)) {
142 | $packed = $this->readFile($cacheFile); // Other ttl strategy than embed
143 | }
144 |
145 | return $this->unpack($packed);
146 | }
147 |
148 | /**
149 | * {@inheritdoc}
150 | */
151 | public function has($key)
152 | {
153 | $cacheFile = $this->getFilename($key);
154 |
155 | if (!file_exists($cacheFile)) {
156 | return false;
157 | }
158 |
159 | $ttl = $this->getTtl($cacheFile);
160 |
161 | if ($ttl <= time()) {
162 | $this->deleteFile($cacheFile);
163 | return false;
164 | }
165 |
166 | return true;
167 | }
168 |
169 | /**
170 | * {@inheritdoc}
171 | */
172 | public function set($key, $value, $ttl = null)
173 | {
174 | $cacheFile = $this->getFilename($key);
175 | $packed = $this->pack($value);
176 |
177 | if (!is_string($packed)) {
178 | throw new UnexpectedValueException("Packer must create a string for the data to be cached to file");
179 | }
180 |
181 | $contents = $this->setTtl($this->ttlToTimestamp($ttl), $packed, $cacheFile);
182 |
183 | return $this->writeFile($cacheFile, $contents);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/Redis.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Julián Gutiérrez
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Exception\UnexpectedValueException;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\SerializePacker;
22 | use Redis as PhpRedis;
23 |
24 | /**
25 | * PHP Redis cache adapter.
26 | *
27 | * Errors are silently ignored but ServerExceptions are **not** caught. To PSR-16 compliant disable the `exception`
28 | * option.
29 | */
30 | class Redis extends AbstractCache
31 | {
32 | /**
33 | * @var PhpRedis
34 | */
35 | protected $client;
36 |
37 | /**
38 | * Redis constructor.
39 | *
40 | * @param PhpRedis $client
41 | */
42 | public function __construct(PhpRedis $client)
43 | {
44 | $this->client = $client;
45 | }
46 |
47 | /**
48 | * Create the default packer for this cache implementation.
49 | *
50 | * @return PackerInterface
51 | */
52 | protected static function createDefaultPacker(): PackerInterface
53 | {
54 | return new SerializePacker();
55 | }
56 |
57 | /**
58 | * Set multiple (mset) with expire
59 | *
60 | * @param array $dictionary
61 | * @param int|null $ttlSeconds
62 | * @return bool
63 | */
64 | protected function msetExpire(array $dictionary, ?int $ttlSeconds): bool
65 | {
66 | if (empty($dictionary)) {
67 | return true;
68 | }
69 |
70 | if (!isset($ttlSeconds)) {
71 | return $this->client->mset($dictionary);
72 | }
73 |
74 | $transaction = $this->client->multi();
75 |
76 | foreach ($dictionary as $key => $value) {
77 | $transaction->set($key, $value, $ttlSeconds);
78 | }
79 |
80 | $responses = $transaction->exec();
81 |
82 | return array_reduce(
83 | $responses,
84 | function ($ok, $response) {
85 | return $ok && $response;
86 | },
87 | true
88 | );
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function get($key, $default = null)
95 | {
96 | $response = $this->client->get($this->keyToId($key));
97 |
98 | return !empty($response) ? $this->unpack($response) : $default;
99 | }
100 |
101 | /**
102 | * {@inheritdoc}
103 | */
104 | public function getMultiple($keys, $default = null)
105 | {
106 | $idKeyPairs = $this->mapKeysToIds($keys);
107 |
108 | $response = $this->client->mget(array_keys($idKeyPairs));
109 |
110 | return array_map(
111 | function ($packed) use ($default) {
112 | return !empty($packed) ? $this->unpack($packed) : $default;
113 | },
114 | array_combine(array_values($idKeyPairs), $response)
115 | );
116 | }
117 |
118 | /**
119 | * {@inheritdoc}
120 | */
121 | public function has($key)
122 | {
123 | return $this->client->exists($this->keyToId($key)) !== 0;
124 | }
125 |
126 | /**
127 | * {@inheritdoc}
128 | */
129 | public function set($key, $value, $ttl = null)
130 | {
131 | $id = $this->keyToId($key);
132 | $packed = $this->pack($value);
133 |
134 | if (!is_string($packed)) {
135 | throw new UnexpectedValueException("Packer must create a string for the data");
136 | }
137 |
138 | $ttlSeconds = $this->ttlToSeconds($ttl);
139 |
140 | if (isset($ttlSeconds) && $ttlSeconds <= 0) {
141 | return $this->client->del($id);
142 | }
143 |
144 | return !isset($ttlSeconds)
145 | ? $this->client->set($id, $packed)
146 | : $this->client->setex($id, $ttlSeconds, $packed);
147 | }
148 |
149 | /**
150 | * {@inheritdoc}
151 | */
152 | public function setMultiple($values, $ttl = null)
153 | {
154 | $this->assertIterable($values, 'values not iterable');
155 |
156 | $dictionary = [];
157 |
158 | foreach ($values as $key => $value) {
159 | $id = $this->keyToId(is_int($key) ? (string)$key : $key);
160 | $packed = $this->pack($value);
161 |
162 | if (!is_string($packed)) {
163 | throw new UnexpectedValueException("Packer must create a string for the data");
164 | }
165 |
166 | $dictionary[$id] = $packed;
167 | }
168 |
169 | $ttlSeconds = $this->ttlToSeconds($ttl);
170 |
171 | if (isset($ttlSeconds) && $ttlSeconds <= 0) {
172 | return $this->client->del(array_keys($dictionary));
173 | }
174 |
175 | return $this->msetExpire($dictionary, $ttlSeconds);
176 | }
177 |
178 | /**
179 | * {@inheritdoc}
180 | */
181 | public function delete($key)
182 | {
183 | $id = $this->keyToId($key);
184 |
185 | return $this->client->del($id) !== false;
186 | }
187 |
188 | /**
189 | * {@inheritdoc}
190 | */
191 | public function deleteMultiple($keys)
192 | {
193 | $ids = array_keys($this->mapKeysToIds($keys));
194 |
195 | return empty($ids) || $this->client->del($ids) !== false;
196 | }
197 |
198 | /**
199 | * {@inheritdoc}
200 | */
201 | public function clear()
202 | {
203 | return $this->client->flushDB();
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/AbstractFile.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | namespace Desarrolla2\Cache;
16 |
17 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
18 | use Desarrolla2\Cache\Option\FilenameTrait as FilenameOption;
19 |
20 | /**
21 | * Abstract class for using files as cache.
22 | *
23 | * @package Desarrolla2\Cache
24 | */
25 | abstract class AbstractFile extends AbstractCache
26 | {
27 | use FilenameOption;
28 |
29 | /**
30 | * @var string
31 | */
32 | protected $cacheDir;
33 |
34 |
35 | /**
36 | * Class constructor
37 | *
38 | * @param string|null $cacheDir
39 | */
40 | public function __construct(?string $cacheDir = null)
41 | {
42 | if (!$cacheDir) {
43 | $cacheDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'cache';
44 | if(!is_dir($cacheDir)) {
45 | mkdir($cacheDir, 0777, true);
46 | }
47 | }
48 |
49 | $this->cacheDir = rtrim($cacheDir, '/');
50 | }
51 |
52 | /**
53 | * Validate the key
54 | *
55 | * @param string $key
56 | * @return void
57 | * @throws InvalidArgumentException
58 | */
59 | protected function assertKey($key): void
60 | {
61 | parent::assertKey($key);
62 |
63 | if (strpos($key, '*')) {
64 | throw new InvalidArgumentException("Key may not contain the character '*'");
65 | }
66 | }
67 |
68 |
69 | /**
70 | * Get the contents of the cache file.
71 | *
72 | * @param string $cacheFile
73 | * @return string
74 | */
75 | protected function readFile(string $cacheFile): string
76 | {
77 | return file_get_contents($cacheFile);
78 | }
79 |
80 | /**
81 | * Read the first line of the cache file.
82 | *
83 | * @param string $cacheFile
84 | * @return string
85 | */
86 | protected function readLine(string $cacheFile): string
87 | {
88 | $fp = fopen($cacheFile, 'r');
89 | $line = fgets($fp);
90 | fclose($fp);
91 |
92 | return $line;
93 | }
94 |
95 | /**
96 | * Create a cache file
97 | *
98 | * @param string $cacheFile
99 | * @param string $contents
100 | * @return bool
101 | */
102 | protected function writeFile(string $cacheFile, string $contents): bool
103 | {
104 | $dir = dirname($cacheFile);
105 |
106 | if ($dir !== $this->cacheDir && !is_dir($dir)) {
107 | mkdir($dir, 0775, true);
108 | }
109 |
110 | return (bool)file_put_contents($cacheFile, $contents);
111 | }
112 |
113 | /**
114 | * Delete a cache file
115 | *
116 | * @param string $file
117 | * @return bool
118 | */
119 | protected function deleteFile(string $file): bool
120 | {
121 | return !is_file($file) || unlink($file);
122 | }
123 |
124 | /**
125 | * Remove all files from a directory.
126 | */
127 | protected function removeFiles(string $dir): bool
128 | {
129 | $success = true;
130 |
131 | $generator = $this->getFilenameOption();
132 | $objects = $this->streamSafeGlob($dir, $generator('*'));
133 |
134 | foreach ($objects as $object) {
135 | $success = $this->deleteFile($object) && $success;
136 | }
137 |
138 | return $success;
139 | }
140 |
141 | /**
142 | * Recursive delete an empty directory.
143 | *
144 | * @param string $dir
145 | */
146 | protected function removeRecursively(string $dir): bool
147 | {
148 | $success = $this->removeFiles($dir);
149 |
150 | $objects = $this->streamSafeGlob($dir, '*');
151 |
152 | foreach ($objects as $object) {
153 | if (!is_dir($object)) {
154 | continue;
155 | }
156 |
157 | if (is_link($object)) {
158 | unlink($object);
159 | } else {
160 | $success = $this->removeRecursively($object) && $success;
161 | rmdir($object);
162 | }
163 | }
164 |
165 | return $success;
166 | }
167 |
168 |
169 | /**
170 | * {@inheritdoc}
171 | */
172 | public function delete($key)
173 | {
174 | $cacheFile = $this->getFilename($key);
175 |
176 | return $this->deleteFile($cacheFile);
177 | }
178 |
179 | /**
180 | * Delete cache directory.
181 | *
182 | * {@inheritdoc}
183 | */
184 | public function clear()
185 | {
186 | $this->removeRecursively($this->cacheDir);
187 |
188 | return true;
189 | }
190 |
191 | /**
192 | * Glob that is safe with streams (vfs for example)
193 | *
194 | * @param string $directory
195 | * @param string $filePattern
196 | * @return array
197 | */
198 | protected function streamSafeGlob(string $directory, string $filePattern): array
199 | {
200 | $filePattern = basename($filePattern);
201 | $files = scandir($directory);
202 | $found = [];
203 |
204 | foreach ($files as $filename) {
205 | if (in_array($filename, ['.', '..'])) {
206 | continue;
207 | }
208 |
209 | if (fnmatch($filePattern, $filename) || fnmatch($filePattern . '.ttl', $filename)) {
210 | $found[] = "{$directory}/{$filename}";
211 | }
212 | }
213 |
214 | return $found;
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/Memcached.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\NopPacker;
22 | use Memcached as MemcachedServer;
23 |
24 | /**
25 | * Memcached
26 | */
27 | class Memcached extends AbstractCache
28 | {
29 | /**
30 | * @var MemcachedServer
31 | */
32 | protected $server;
33 |
34 | /**
35 | * @param MemcachedServer $server
36 | */
37 | public function __construct(MemcachedServer $server)
38 | {
39 | $this->server = $server;
40 | }
41 |
42 |
43 | /**
44 | * Create the default packer for this cache implementation
45 | *
46 | * @return PackerInterface
47 | */
48 | protected static function createDefaultPacker(): PackerInterface
49 | {
50 | return new NopPacker();
51 | }
52 |
53 | /**
54 | * Validate the key
55 | *
56 | * @param string $key
57 | * @return void
58 | * @throws InvalidArgumentException
59 | */
60 | protected function assertKey($key): void
61 | {
62 | parent::assertKey($key);
63 |
64 | if (strlen($key) > 250) {
65 | throw new InvalidArgumentException("Key to long, max 250 characters");
66 | }
67 | }
68 |
69 | /**
70 | * Pack all values and turn keys into ids
71 | *
72 | * @param iterable $values
73 | * @return array
74 | */
75 | protected function packValues(iterable $values): array
76 | {
77 | $packed = [];
78 |
79 | foreach ($values as $key => $value) {
80 | $this->assertKey(is_int($key) ? (string)$key : $key);
81 | $packed[$key] = $this->pack($value);
82 | }
83 |
84 | return $packed;
85 | }
86 |
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function get($key, $default = null)
92 | {
93 | $this->assertKey($key);
94 |
95 | $data = $this->server->get($key);
96 |
97 | if ($this->server->getResultCode() !== MemcachedServer::RES_SUCCESS) {
98 | return $default;
99 | }
100 |
101 | return $this->unpack($data);
102 | }
103 |
104 | /**
105 | * {@inheritdoc}
106 | */
107 | public function has($key)
108 | {
109 | $this->assertKey($key);
110 | $this->server->get($key);
111 |
112 | $result = $this->server->getResultCode();
113 |
114 | return $result === MemcachedServer::RES_SUCCESS;
115 | }
116 |
117 | /**
118 | * {@inheritdoc}
119 | */
120 | public function set($key, $value, $ttl = null)
121 | {
122 | $this->assertKey($key);
123 |
124 | $packed = $this->pack($value);
125 | $ttlTime = $this->ttlToMemcachedTime($ttl);
126 |
127 | if ($ttlTime === false) {
128 | return $this->delete($key);
129 | }
130 |
131 | $success = $this->server->set($key, $packed, $ttlTime);
132 |
133 | return $success;
134 | }
135 |
136 | /**
137 | * {@inheritdoc}
138 | */
139 | public function delete($key)
140 | {
141 | $this->server->delete($this->keyToId($key));
142 |
143 | $result = $this->server->getResultCode();
144 |
145 | return $result === MemcachedServer::RES_SUCCESS || $result === MemcachedServer::RES_NOTFOUND;
146 | }
147 |
148 | /**
149 | * {@inheritdoc}
150 | */
151 | public function getMultiple($keys, $default = null)
152 | {
153 | $this->assertIterable($keys, 'keys not iterable');
154 | $keysArr = is_array($keys) ? $keys : iterator_to_array($keys, false);
155 | array_walk($keysArr, [$this, 'assertKey']);
156 |
157 | $result = $this->server->getMulti($keysArr);
158 |
159 | if ($result === false) {
160 | return false;
161 | }
162 |
163 | $items = array_fill_keys($keysArr, $default);
164 |
165 | foreach ($result as $key => $value) {
166 | $items[$key] = $this->unpack($value);
167 | }
168 |
169 | return $items;
170 | }
171 |
172 | /**
173 | * {@inheritdoc}
174 | */
175 | public function setMultiple($values, $ttl = null)
176 | {
177 | $this->assertIterable($values, 'values not iterable');
178 |
179 | $packed = $this->packValues($values);
180 | $ttlTime = $this->ttlToMemcachedTime($ttl);
181 |
182 | if ($ttlTime === false) {
183 | return $this->server->deleteMulti(array_keys($packed));
184 | }
185 |
186 | return $this->server->setMulti($packed, $ttlTime);
187 | }
188 |
189 | /**
190 | * {@inheritdoc}
191 | */
192 | public function clear()
193 | {
194 | return $this->server->flush();
195 | }
196 |
197 |
198 | /**
199 | * Convert ttl to timestamp or seconds.
200 | *
201 | * @see http://php.net/manual/en/memcached.expiration.php
202 | *
203 | * @param null|int|\DateInterval $ttl
204 | * @return int|null
205 | * @throws InvalidArgumentException
206 | */
207 | protected function ttlToMemcachedTime($ttl)
208 | {
209 | $seconds = $this->ttlToSeconds($ttl);
210 |
211 | if ($seconds <= 0) {
212 | return isset($seconds) ? false : 0;
213 | }
214 |
215 | /* 2592000 seconds = 30 days */
216 | return $seconds <= 2592000 ? $seconds : $this->ttlToTimestamp($ttl);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/Predis.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\AbstractCache;
20 | use Desarrolla2\Cache\Exception\UnexpectedValueException;
21 | use Desarrolla2\Cache\Packer\PackerInterface;
22 | use Desarrolla2\Cache\Packer\SerializePacker;
23 | use Predis\Client;
24 | use Predis\Response\ServerException;
25 | use Predis\Response\Status;
26 | use Predis\Response\ErrorInterface;
27 |
28 | /**
29 | * Predis cache adapter.
30 | *
31 | * Errors are silently ignored but ServerExceptions are **not** caught. To PSR-16 compliant disable the `exception`
32 | * option.
33 | */
34 | class Predis extends AbstractCache
35 | {
36 | /**
37 | * @var Client
38 | */
39 | protected $predis;
40 |
41 | /**
42 | * Class constructor
43 | * @see predis documentation about how know your configuration https://github.com/nrk/predis
44 | *
45 | * @param Client $client
46 | */
47 | public function __construct(Client $client)
48 | {
49 | $this->predis = $client;
50 | }
51 |
52 | /**
53 | * Create the default packer for this cache implementation.
54 | *
55 | * @return PackerInterface
56 | */
57 | protected static function createDefaultPacker(): PackerInterface
58 | {
59 | return new SerializePacker();
60 | }
61 |
62 |
63 | /**
64 | * Run a predis command.
65 | *
66 | * @param string $cmd
67 | * @param mixed ...$args
68 | * @return mixed|bool
69 | */
70 | protected function execCommand(string $cmd, ...$args)
71 | {
72 | $command = $this->predis->createCommand($cmd, $args);
73 | $response = $this->predis->executeCommand($command);
74 |
75 | if ($response instanceof ErrorInterface) {
76 | return false;
77 | }
78 |
79 | if ($response instanceof Status) {
80 | return $response->getPayload() === 'OK';
81 | }
82 |
83 | return $response;
84 | }
85 |
86 | /**
87 | * Set multiple (mset) with expire
88 | *
89 | * @param array $dictionary
90 | * @param int|null $ttlSeconds
91 | * @return bool
92 | */
93 | protected function msetExpire(array $dictionary, ?int $ttlSeconds): bool
94 | {
95 | if (empty($dictionary)) {
96 | return true;
97 | }
98 |
99 | if (!isset($ttlSeconds)) {
100 | return $this->execCommand('MSET', $dictionary);
101 | }
102 |
103 | $transaction = $this->predis->transaction();
104 |
105 | foreach ($dictionary as $key => $value) {
106 | $transaction->set($key, $value, 'EX', $ttlSeconds);
107 | }
108 |
109 | try {
110 | $responses = $transaction->execute();
111 | } catch (ServerException $e) {
112 | return false;
113 | }
114 |
115 | $ok = array_reduce($responses, function($ok, $response) {
116 | return $ok && $response instanceof Status && $response->getPayload() === 'OK';
117 | }, true);
118 |
119 | return $ok;
120 | }
121 |
122 |
123 | /**
124 | * {@inheritdoc}
125 | */
126 | public function get($key, $default = null)
127 | {
128 | $id = $this->keyToId($key);
129 | $response = $this->execCommand('GET', $id);
130 |
131 | return !empty($response) ? $this->unpack($response) : $default;
132 | }
133 |
134 | /**
135 | * {@inheritdoc}
136 | */
137 | public function getMultiple($keys, $default = null)
138 | {
139 | $idKeyPairs = $this->mapKeysToIds($keys);
140 | $ids = array_keys($idKeyPairs);
141 |
142 | $response = $this->execCommand('MGET', $ids);
143 |
144 | if ($response === false) {
145 | return false;
146 | }
147 |
148 | $items = [];
149 | $packedItems = array_combine(array_values($idKeyPairs), $response);
150 |
151 | foreach ($packedItems as $key => $packed) {
152 | $items[$key] = isset($packed) ? $this->unpack($packed) : $default;
153 | }
154 |
155 | return $items;
156 | }
157 |
158 | /**
159 | * {@inheritdoc}
160 | */
161 | public function has($key)
162 | {
163 | return $this->execCommand('EXISTS', $this->keyToId($key));
164 | }
165 |
166 | /**
167 | * {@inheritdoc}
168 | */
169 | public function set($key, $value, $ttl = null)
170 | {
171 | $id = $this->keyToId($key);
172 | $packed = $this->pack($value);
173 |
174 | if (!is_string($packed)) {
175 | throw new UnexpectedValueException("Packer must create a string for the data");
176 | }
177 |
178 | $ttlSeconds = $this->ttlToSeconds($ttl);
179 |
180 | if (isset($ttlSeconds) && $ttlSeconds <= 0) {
181 | return $this->execCommand('DEL', [$id]);
182 | }
183 |
184 | return !isset($ttlSeconds)
185 | ? $this->execCommand('SET', $id, $packed)
186 | : $this->execCommand('SETEX', $id, $ttlSeconds, $packed);
187 | }
188 |
189 | /**
190 | * {@inheritdoc}
191 | */
192 | public function setMultiple($values, $ttl = null)
193 | {
194 | $this->assertIterable($values, 'values not iterable');
195 |
196 | $dictionary = [];
197 |
198 | foreach ($values as $key => $value) {
199 | $id = $this->keyToId(is_int($key) ? (string)$key : $key);
200 | $packed = $this->pack($value);
201 |
202 | if (!is_string($packed)) {
203 | throw new UnexpectedValueException("Packer must create a string for the data");
204 | }
205 |
206 | $dictionary[$id] = $packed;
207 | }
208 |
209 | $ttlSeconds = $this->ttlToSeconds($ttl);
210 |
211 | if (isset($ttlSeconds) && $ttlSeconds <= 0) {
212 | return $this->execCommand('DEL', array_keys($dictionary));
213 | }
214 |
215 | return $this->msetExpire($dictionary, $ttlSeconds);
216 | }
217 |
218 | /**
219 | * {@inheritdoc}
220 | */
221 | public function delete($key)
222 | {
223 | $id = $this->keyToId($key);
224 |
225 | return $this->execCommand('DEL', [$id]) !== false;
226 | }
227 |
228 | /**
229 | * {@inheritdoc}
230 | */
231 | public function deleteMultiple($keys)
232 | {
233 | $ids = array_keys($this->mapKeysToIds($keys));
234 |
235 | return empty($ids) || $this->execCommand('DEL', $ids) !== false;
236 | }
237 |
238 | /**
239 | * {@inheritdoc}
240 | */
241 | public function clear()
242 | {
243 | return $this->execCommand('FLUSHDB');
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Desarolla2 Cache
2 |
3 | A **simple cache** library, implementing the [PSR-16](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) standard using **immutable** objects.
4 |
5 | 
6 |
7 | Caching is typically used throughout an applicatiton. Immutability ensure that modifying the cache behaviour in one
8 | location doesn't result in unexpected behaviour due to changes in unrelated code.
9 |
10 | _Desarolla2 Cache aims to be the most complete, correct and best performing PSR-16 implementation available._
11 |
12 | [![Latest version][ico-version]][link-packagist]
13 | [![Latest version][ico-pre-release]][link-packagist]
14 | [![Software License][ico-license]][link-license]
15 | [![Build Status][ico-github-actions]][link-github-actions]
16 | [![Coverage Status][ico-coverage]][link-scrutinizer]
17 | [![Quality Score][ico-code-quality]][link-scrutinizer]
18 | [![Total Downloads][ico-downloads]][link-downloads]
19 | [![Today Downloads][ico-today-downloads]][link-downloads]
20 | [![Gitter][ico-gitter]][link-gitter]
21 |
22 |
23 | ## Installation
24 |
25 | ```
26 | composer require desarrolla2/cache
27 | ```
28 |
29 | ## Usage
30 |
31 |
32 | ``` php
33 | use Desarrolla2\Cache\Memory as Cache;
34 |
35 | $cache = new Cache();
36 |
37 | $value = $cache->get('key');
38 |
39 | if (!isset($value)) {
40 | $value = do_something();
41 | $cache->set('key', $value, 3600);
42 | }
43 |
44 | echo $value;
45 | ```
46 |
47 | ## Adapters
48 |
49 | * [Apcu](docs/implementations/apcu.md)
50 | * [File](docs/implementations/file.md)
51 | * [Memcached](docs/implementations/memcached.md)
52 | * [Memory](docs/implementations/memory.md)
53 | * [MongoDB](docs/implementations/mongodb.md)
54 | * [Mysqli](docs/implementations/mysqli.md)
55 | * [NotCache](docs/implementations/notcache.md)
56 | * [PhpFile](docs/implementations/phpfile.md)
57 | * [Predis](docs/implementations/predis.md)
58 | * [Redis](docs/implementations/redis.md)
59 |
60 | The following implementation allows you to combine cache adapters.
61 |
62 | * [Chain](docs/implementations/chain.md)
63 |
64 | ### Options
65 |
66 | You can set options for cache using the `withOption` or `withOptions` method.
67 | Note that all cache objects are immutable, setting an option creates a new
68 | object.
69 |
70 | #### TTL
71 |
72 | All cache implementations support the `ttl` option. This sets the default
73 | time (in seconds) that cache will survive. It defaults to one hour (3600
74 | seconds).
75 |
76 | Setting the TTL to 0 or a negative number, means the cache should live forever.
77 |
78 | ## Methods
79 |
80 | ### PSR-16 methods
81 |
82 | Each cache implementation has the following `Psr\SimpleCache\CacheInterface`
83 | methods:
84 |
85 | ##### `get(string $key [, mixed $default])`
86 | Retrieve the value corresponding to a provided key
87 |
88 | ##### `has(string $key)`
89 | Retrieve the if value corresponding to a provided key exist
90 |
91 | ##### `set(string $key, mixed $value [, int $ttl])`
92 | Add a value to the cache under a unique key
93 |
94 | ##### `delete(string $key)`
95 | Delete a value from the cache
96 |
97 | ##### `clear()`
98 | Clear all cache
99 |
100 | ##### `getMultiple(array $keys)`
101 | Obtains multiple cache items by their unique keys
102 |
103 | ##### `setMultiple(array $values [, int $ttl])`
104 | Persists a set of key => value pairs in the cache
105 |
106 | ##### `deleteMultiple(array $keys)`
107 | Deletes multiple cache items in a single operation
108 |
109 | ### Additional methods
110 |
111 | The `Desarrolla2\Cache\CacheInterface` also has the following methods:
112 |
113 | ##### `withOption(string $key, string $value)`
114 | Set option for implementation. Creates a new instance.
115 |
116 | ##### `withOptions(array $options)`
117 | Set multiple options for implementation. Creates a new instance.
118 |
119 | ##### `getOption(string $key)`
120 | Get option for implementation.
121 |
122 |
123 | ## Packers
124 |
125 | Cache objects typically hold a `Desarrolla2\Cache\Packer\PackerInterface`
126 | object. By default, packing is done using `serialize` and `unserialize`.
127 |
128 | Available packers are:
129 |
130 | * `SerializePacker` using `serialize` and `unserialize`
131 | * `JsonPacker` using `json_encode` and `json_decode`
132 | * `NopPacker` does no packing
133 | * `MongoDBBinaryPacker` using `serialize` and `unserialize` to store as [BSON Binary](http://php.net/manual/en/class.mongodb-bson-binary.php)
134 |
135 | #### PSR-16 incompatible packers
136 |
137 | The `JsonPacker` does not fully comply with PSR-16, as packing and
138 | unpacking an object will probably not result in an object of the same class.
139 |
140 | The `NopPacker` is intended when caching string data only (like HTML output) or
141 | if the caching backend supports structured data. Using it when storing objects
142 | will might give unexpected results.
143 |
144 | ## Contributors
145 |
146 | [](https://github.com/desarrolla2)
147 | Twitter: [@desarrolla2](https://twitter.com/desarrolla2)\
148 | [](https://github.com/jasny)
149 | Twitter: [@ArnoldDaniels](https://twitter.com/ArnoldDaniels)
150 |
151 | [ico-version]: https://img.shields.io/packagist/v/desarrolla2/Cache.svg?style=flat-square
152 | [ico-pre-release]: https://img.shields.io/packagist/vpre/desarrolla2/Cache.svg?style=flat-square
153 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
154 | [ico-travis]: https://img.shields.io/travis/desarrolla2/Cache/master.svg?style=flat-square
155 | [ico-coveralls]: https://img.shields.io/coveralls/desarrolla2/Cache/master.svg?style=flat-square
156 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/desarrolla2/cache.svg?style=flat-square
157 | [ico-coverage]: https://scrutinizer-ci.com/g/desarrolla2/Cache/badges/coverage.png?b=master
158 | [ico-sensiolabs]: https://img.shields.io/sensiolabs/i/5f139261-1ac1-4559-846a-723e09319a88.svg?style=flat-square
159 | [ico-downloads]: https://img.shields.io/packagist/dt/desarrolla2/cache.svg?style=flat-square
160 | [ico-today-downloads]: https://img.shields.io/packagist/dd/desarrolla2/cache.svg?style=flat-square
161 | [ico-gitter]: https://img.shields.io/badge/GITTER-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat-square
162 | [ico-github-actions]: https://github.com/desarrolla2/Cache/workflows/PHP/badge.svg
163 |
164 | [link-packagist]: https://packagist.org/packages/desarrolla2/cache
165 | [link-license]: http://hassankhan.mit-license.org
166 | [link-travis]: https://travis-ci.org/desarrolla2/Cache
167 | [link-github-actions]: https://github.com/desarrolla2/Cache/actions
168 | [link-coveralls]: https://coveralls.io/github/desarrolla2/Cache
169 | [link-scrutinizer]: https://scrutinizer-ci.com/g/desarrolla2/cache
170 | [link-sensiolabs]: https://insight.sensiolabs.com/projects/5f139261-1ac1-4559-846a-723e09319a88
171 | [link-downloads]: https://packagist.org/packages/desarrolla2/cache
172 | [link-gitter]: https://gitter.im/desarrolla2/Cache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
173 |
--------------------------------------------------------------------------------
/src/MongoDB.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Packer\PackerInterface;
20 | use Desarrolla2\Cache\Packer\MongoDBBinaryPacker;
21 | use Desarrolla2\Cache\Option\InitializeTrait as InitializeOption;
22 | use MongoDB\Collection;
23 | use MongoDB\BSON\UTCDatetime as BSONUTCDateTime;
24 | use MongoDB\Driver\Exception\RuntimeException as MongoDBRuntimeException;
25 |
26 | /**
27 | * MongoDB cache implementation
28 | */
29 | class MongoDB extends AbstractCache
30 | {
31 | use InitializeOption;
32 |
33 | /**
34 | * @var Collection
35 | */
36 | protected $collection;
37 |
38 | /**
39 | * Class constructor
40 | *
41 | * @param Collection $collection
42 | */
43 | public function __construct(Collection $collection)
44 | {
45 | $this->collection = $collection;
46 | }
47 |
48 | /**
49 | * Initialize the DB collection.
50 | * Set TTL index.
51 | */
52 | protected function initialize(): void
53 | {
54 | $this->collection->createIndex(['ttl' => 1], ['expireAfterSeconds' => 0]);
55 | }
56 |
57 |
58 | /**
59 | * Create the default packer for this cache implementation.
60 | *
61 | * @return PackerInterface
62 | */
63 | protected static function createDefaultPacker(): PackerInterface
64 | {
65 | return new MongoDBBinaryPacker();
66 | }
67 |
68 | /**
69 | * Get filter for key and ttl.
70 | *
71 | * @param string|iterable $key
72 | * @return array
73 | */
74 | protected function filter($key)
75 | {
76 | if (is_array($key)) {
77 | $key = ['$in' => $key];
78 | }
79 |
80 | return [
81 | '_id' => $key,
82 | '$or' => [
83 | ['ttl' => ['$gt' => new BSONUTCDateTime($this->currentTimestamp() * 1000)]],
84 | ['ttl' => null]
85 | ]
86 | ];
87 | }
88 |
89 | /**
90 | * {@inheritdoc }
91 | */
92 | public function get($key, $default = null)
93 | {
94 | $filter = $this->filter($this->keyToId($key));
95 |
96 | try {
97 | $data = $this->collection->findOne($filter);
98 | } catch (MongoDBRuntimeException $e) {
99 | return $default;
100 | }
101 |
102 | return isset($data) ? $this->unpack($data['value']) : $default;
103 | }
104 |
105 | /**
106 | * {@inheritdoc}
107 | */
108 | public function getMultiple($keys, $default = null)
109 | {
110 | $idKeyPairs = $this->mapKeysToIds($keys);
111 |
112 | if (empty($idKeyPairs)) {
113 | return [];
114 | }
115 |
116 | $filter = $this->filter(array_keys($idKeyPairs));
117 | $items = array_fill_keys(array_values($idKeyPairs), $default);
118 |
119 | try {
120 | $rows = $this->collection->find($filter);
121 | } catch (MongoDBRuntimeException $e) {
122 | return $items;
123 | }
124 |
125 | foreach ($rows as $row) {
126 | $id = $row['_id'];
127 | $key = $idKeyPairs[$id];
128 |
129 | $items[$key] = $this->unpack($row['value']);
130 | }
131 |
132 | return $items;
133 | }
134 |
135 | /**
136 | * {@inheritdoc }
137 | */
138 | public function has($key)
139 | {
140 | $filter = $this->filter($this->keyToId($key));
141 |
142 | try {
143 | $count = $this->collection->count($filter);
144 | } catch (MongoDBRuntimeException $e) {
145 | return false;
146 | }
147 |
148 | return $count > 0;
149 | }
150 |
151 | /**
152 | * {@inheritdoc }
153 | */
154 | public function set($key, $value, $ttl = null)
155 | {
156 | $id = $this->keyToId($key);
157 |
158 | $item = [
159 | '_id' => $id,
160 | 'ttl' => $this->getTtlBSON($ttl),
161 | 'value' => $this->pack($value)
162 | ];
163 |
164 | try {
165 | $this->collection->replaceOne(['_id' => $id], $item, ['upsert' => true]);
166 | } catch (MongoDBRuntimeException $e) {
167 | return false;
168 | }
169 |
170 | return true;
171 | }
172 |
173 | /**
174 | * {@inheritdoc}
175 | */
176 | public function setMultiple($values, $ttl = null)
177 | {
178 | $this->assertIterable($values, 'values not iterable');
179 |
180 | if (empty($values)) {
181 | return true;
182 | }
183 |
184 | $bsonTtl = $this->getTtlBSON($ttl);
185 | $items = [];
186 |
187 | foreach ($values as $key => $value) {
188 | $id = $this->keyToId(is_int($key) ? (string)$key : $key);
189 |
190 | $items[] = [
191 | 'replaceOne' => [
192 | ['_id' => $id],
193 | [
194 | '_id' => $id,
195 | 'ttl' => $bsonTtl,
196 | 'value' => $this->pack($value)
197 | ],
198 | [ 'upsert' => true ]
199 | ]
200 | ];
201 | }
202 |
203 | try {
204 | $this->collection->bulkWrite($items);
205 | } catch (MongoDBRuntimeException $e) {
206 | return false;
207 | }
208 |
209 | return true;
210 | }
211 |
212 | /**
213 | * {@inheritdoc}
214 | */
215 | public function delete($key)
216 | {
217 | $id = $this->keyToId($key);
218 |
219 | try {
220 | $this->collection->deleteOne(['_id' => $id]);
221 | } catch (MongoDBRuntimeException $e) {
222 | return false;
223 | }
224 |
225 | return true;
226 | }
227 |
228 | /**
229 | * {@inheritdoc}
230 | */
231 | public function deleteMultiple($keys)
232 | {
233 | $idKeyPairs = $this->mapKeysToIds($keys);
234 |
235 | try {
236 | if (!empty($idKeyPairs)) {
237 | $this->collection->deleteMany(['_id' => ['$in' => array_keys($idKeyPairs)]]);
238 | }
239 | } catch (MongoDBRuntimeException $e) {
240 | return false;
241 | }
242 |
243 | return true;
244 | }
245 |
246 | /**
247 | * {@inheritdoc}
248 | */
249 | public function clear()
250 | {
251 | try {
252 | $this->collection->drop();
253 | } catch (MongoDBRuntimeException $e) {
254 | return false;
255 | }
256 |
257 | $this->requireInitialization();
258 |
259 | return true;
260 | }
261 |
262 |
263 | /**
264 | * Get TTL as Date type BSON object
265 | *
266 | * @param null|int|\DateInterval $ttl
267 | * @return BSONUTCDatetime|null
268 | */
269 | protected function getTtlBSON($ttl): ?BSONUTCDatetime
270 | {
271 | return isset($ttl) ? new BSONUTCDateTime($this->ttlToTimestamp($ttl) * 1000) : null;
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/tests/ChainTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | namespace Desarrolla2\Test\Cache;
15 |
16 | use Desarrolla2\Cache\Chain as CacheChain;
17 | use Desarrolla2\Cache\Memory as MemoryCache;
18 |
19 | /**
20 | * ChainTest
21 | */
22 | class ChainTest extends AbstractCacheTest
23 | {
24 | public function createSimpleCache()
25 | {
26 | $adapters = [new MemoryCache()]; // For the general PSR-16 tests, we don't need more than 1 adapter
27 |
28 | return new CacheChain($adapters);
29 | }
30 |
31 |
32 | public function tearDown(): void
33 | {
34 | // No need to clear cache, as the adapters don't persist between tests.
35 | }
36 |
37 |
38 | public function testChainSet()
39 | {
40 | $adapter1 = $this->createMock(MemoryCache::class);
41 | $adapter1->expects($this->once())->method('set')->with("foo", "bar", 300);
42 |
43 | $adapter2 = $this->createMock(MemoryCache::class);
44 | $adapter2->expects($this->once())->method('set')->with("foo", "bar", 300);
45 |
46 | $cache = new CacheChain([$adapter1, $adapter2]);
47 |
48 | $cache->set("foo", "bar", 300);
49 | }
50 |
51 | public function testChainSetMultiple()
52 | {
53 | $adapter1 = $this->createMock(MemoryCache::class);
54 | $adapter1->expects($this->once())->method('setMultiple')->with(["foo" => 1, "bar" => 2], 300);
55 |
56 | $adapter2 = $this->createMock(MemoryCache::class);
57 | $adapter2->expects($this->once())->method('setMultiple')->with(["foo" => 1, "bar" => 2], 300);
58 |
59 | $cache = new CacheChain([$adapter1, $adapter2]);
60 |
61 | $cache->setMultiple(["foo" => 1, "bar" => 2], 300);
62 | }
63 |
64 |
65 | public function testChainGetFirst()
66 | {
67 | $adapter1 = $this->createMock(MemoryCache::class);
68 | $adapter1->expects($this->once())->method('get')->with("foo")->willReturn("bar");
69 |
70 | $adapter2 = $this->createMock(MemoryCache::class);
71 | $adapter2->expects($this->never())->method('get');
72 |
73 | $cache = new CacheChain([$adapter1, $adapter2]);
74 |
75 | $this->assertEquals("bar", $cache->get("foo", 42));
76 | }
77 |
78 | public function testChainGetSecond()
79 | {
80 | $adapter1 = $this->createMock(MemoryCache::class);
81 | $adapter1->expects($this->once())->method('get')->with("foo")->willReturn(null);
82 |
83 | $adapter2 = $this->createMock(MemoryCache::class);
84 | $adapter2->expects($this->once())->method('get')->with("foo")->willReturn("car");
85 |
86 | $cache = new CacheChain([$adapter1, $adapter2]);
87 |
88 | $this->assertEquals("car", $cache->get("foo", 42));
89 | }
90 |
91 | public function testChainGetNone()
92 | {
93 | $adapter1 = $this->createMock(MemoryCache::class);
94 | $adapter1->expects($this->once())->method('get')->with("foo")->willReturn(null);
95 |
96 | $adapter2 = $this->createMock(MemoryCache::class);
97 | $adapter2->expects($this->once())->method('get')->with("foo")->willReturn(null);
98 |
99 | $cache = new CacheChain([$adapter1, $adapter2]);
100 |
101 | $this->assertEquals(42, $cache->get("foo", 42));
102 | }
103 |
104 |
105 | public function testChainGetMultipleFirst()
106 | {
107 | $adapter1 = $this->createMock(MemoryCache::class);
108 | $adapter1->expects($this->once())->method('getMultiple')->with(["foo", "bar"])
109 | ->willReturn(["foo" => 1, "bar" => 2]);
110 |
111 | $adapter2 = $this->createMock(MemoryCache::class);
112 | $adapter2->expects($this->never())->method('getMultiple');
113 |
114 | $cache = new CacheChain([$adapter1, $adapter2]);
115 |
116 | $this->assertEquals(["foo" => 1, "bar" => 2], $cache->getMultiple(["foo", "bar"]));
117 | }
118 |
119 | public function testChainGetMultipleMixed()
120 | {
121 | $adapter1 = $this->createMock(MemoryCache::class);
122 | $adapter1->expects($this->once())->method('getMultiple')
123 | ->with($this->equalTo(["foo", "bar", "wux", "lot"]))
124 | ->willReturn(["foo" => null, "bar" => 2, "wux" => null, "lot" => null]);
125 |
126 | $adapter2 = $this->createMock(MemoryCache::class);
127 | $adapter2->expects($this->once())->method('getMultiple')
128 | ->with($this->equalTo(["foo", "wux", "lot"]))
129 | ->willReturn(["foo" => 11, "wux" => 15, "lot" => null]);
130 |
131 | $cache = new CacheChain([$adapter1, $adapter2]);
132 |
133 | $expected = ["foo" => 11, "bar" => 2, "wux" => 15, "lot" => 42];
134 | $this->assertEquals($expected, $cache->getMultiple(["foo", "bar", "wux", "lot"], 42));
135 | }
136 |
137 |
138 | public function testChainHasFirst()
139 | {
140 | $adapter1 = $this->createMock(MemoryCache::class);
141 | $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(true);
142 |
143 | $adapter2 = $this->createMock(MemoryCache::class);
144 | $adapter2->expects($this->never())->method('has');
145 |
146 | $cache = new CacheChain([$adapter1, $adapter2]);
147 |
148 | $this->assertTrue($cache->has("foo"));
149 | }
150 |
151 | public function testChainHasSecond()
152 | {
153 | $adapter1 = $this->createMock(MemoryCache::class);
154 | $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(false);
155 |
156 | $adapter2 = $this->createMock(MemoryCache::class);
157 | $adapter2->expects($this->once())->method('has')->with("foo")->willReturn(true);
158 |
159 | $cache = new CacheChain([$adapter1, $adapter2]);
160 |
161 | $this->assertTrue($cache->has("foo"));
162 | }
163 |
164 | public function testChainHasNone()
165 | {
166 | $adapter1 = $this->createMock(MemoryCache::class);
167 | $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(false);
168 |
169 | $adapter2 = $this->createMock(MemoryCache::class);
170 | $adapter2->expects($this->once())->method('has')->with("foo")->willReturn(false);
171 |
172 | $cache = new CacheChain([$adapter1, $adapter2]);
173 |
174 | $this->assertFalse($cache->has("foo"));
175 | }
176 |
177 |
178 | public function testChainDelete()
179 | {
180 | $adapter1 = $this->createMock(MemoryCache::class);
181 | $adapter1->expects($this->once())->method('delete')->with("foo");
182 |
183 | $adapter2 = $this->createMock(MemoryCache::class);
184 | $adapter2->expects($this->once())->method('delete')->with("foo");
185 |
186 | $cache = new CacheChain([$adapter1, $adapter2]);
187 |
188 | $cache->delete("foo");
189 | }
190 |
191 | public function testChainDeleteMultiple()
192 | {
193 | $adapter1 = $this->createMock(MemoryCache::class);
194 | $adapter1->expects($this->once())->method('deleteMultiple')->with(["foo", "bar"]);
195 |
196 | $adapter2 = $this->createMock(MemoryCache::class);
197 | $adapter2->expects($this->once())->method('deleteMultiple')->with(["foo", "bar"]);
198 |
199 | $cache = new CacheChain([$adapter1, $adapter2]);
200 |
201 | $cache->deleteMultiple(["foo", "bar"]);
202 | }
203 |
204 | public function testChainClear()
205 | {
206 | $adapter1 = $this->createMock(MemoryCache::class);
207 | $adapter1->expects($this->once())->method('clear');
208 |
209 | $adapter2 = $this->createMock(MemoryCache::class);
210 | $adapter2->expects($this->once())->method('clear');
211 |
212 | $cache = new CacheChain([$adapter1, $adapter2]);
213 |
214 | $cache->clear();
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/AbstractCache.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Arnold Daniels
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace Desarrolla2\Cache;
18 |
19 | use Desarrolla2\Cache\Option\PrefixTrait as PrefixOption;
20 | use Desarrolla2\Cache\Option\TtlTrait as TtlOption;
21 | use Desarrolla2\Cache\Packer\PackingTrait as Packing;
22 | use Desarrolla2\Cache\Exception\InvalidArgumentException;
23 | use DateTimeImmutable;
24 | use DateInterval;
25 | use Traversable;
26 |
27 | /**
28 | * AbstractAdapter
29 | */
30 | abstract class AbstractCache implements CacheInterface
31 | {
32 | use PrefixOption;
33 | use TtlOption;
34 | use Packing;
35 |
36 | /**
37 | * Make a clone of this object.
38 | *
39 | * @return static
40 | */
41 | protected function cloneSelf(): self
42 | {
43 | return clone $this;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function withOption(string $key, $value): self
50 | {
51 | return $this->withOptions([$key => $value]);
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function withOptions(array $options): self
58 | {
59 | $cache = $this->cloneSelf();
60 |
61 | foreach ($options as $key => $value) {
62 | $method = "set" . str_replace('-', '', $key) . "Option";
63 |
64 | if (empty($key) || !method_exists($cache, $method)) {
65 | throw new InvalidArgumentException("unknown option '$key'");
66 | }
67 |
68 | $cache->$method($value);
69 | }
70 |
71 | return $cache;
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function getOption($key)
78 | {
79 | $method = "get" . str_replace('-', '', $key) . "Option";
80 |
81 | if (empty($key) || !method_exists($this, $method)) {
82 | throw new InvalidArgumentException("unknown option '$key'");
83 | }
84 |
85 | return $this->$method();
86 | }
87 |
88 |
89 | /**
90 | * Validate the key
91 | *
92 | * @param string $key
93 | * @return void
94 | * @throws InvalidArgumentException
95 | */
96 | protected function assertKey($key): void
97 | {
98 | if (!is_string($key)) {
99 | $type = (is_object($key) ? get_class($key) . ' ' : '') . gettype($key);
100 | throw new InvalidArgumentException("Expected key to be a string, not $type");
101 | }
102 |
103 | if ($key === '' || preg_match('~[{}()/\\\\@:]~', $key)) {
104 | throw new InvalidArgumentException("Invalid key '$key'");
105 | }
106 | }
107 |
108 | /**
109 | * Assert that the keys are an array or traversable
110 | *
111 | * @param iterable $subject
112 | * @param string $msg
113 | * @return void
114 | * @throws InvalidArgumentException if subject are not iterable
115 | */
116 | protected function assertIterable($subject, $msg): void
117 | {
118 | $iterable = function_exists('is_iterable')
119 | ? is_iterable($subject)
120 | : is_array($subject) || $subject instanceof Traversable;
121 |
122 | if (!$iterable) {
123 | throw new InvalidArgumentException($msg);
124 | }
125 | }
126 |
127 | /**
128 | * Turn the key into a cache identifier
129 | *
130 | * @param string $key
131 | * @return string
132 | * @throws InvalidArgumentException
133 | */
134 | protected function keyToId($key): string
135 | {
136 | $this->assertKey($key);
137 |
138 | return sprintf('%s%s', $this->prefix, $key);
139 | }
140 |
141 | /**
142 | * Create a map with keys and ids
143 | *
144 | * @param iterable $keys
145 | * @return array
146 | * @throws InvalidArgumentException
147 | */
148 | protected function mapKeysToIds($keys): array
149 | {
150 | $this->assertIterable($keys, 'keys not iterable');
151 |
152 | $map = [];
153 |
154 | foreach ($keys as $key) {
155 | $id = $this->keyToId($key);
156 | $map[$id] = $key;
157 | }
158 |
159 | return $map;
160 | }
161 |
162 |
163 | /**
164 | * Pack all values and turn keys into ids
165 | *
166 | * @param iterable $values
167 | * @return array
168 | */
169 | protected function packValues(iterable $values): array
170 | {
171 | $packed = [];
172 |
173 | foreach ($values as $key => $value) {
174 | $id = $this->keyToId(is_int($key) ? (string)$key : $key);
175 | $packed[$id] = $this->pack($value);
176 | }
177 |
178 | return $packed;
179 | }
180 |
181 |
182 |
183 | /**
184 | * {@inheritdoc}
185 | */
186 | public function getMultiple($keys, $default = null)
187 | {
188 | $this->assertIterable($keys, 'keys not iterable');
189 |
190 | $result = [];
191 |
192 | foreach ($keys as $key) {
193 | $result[$key] = $this->get($key, $default);
194 | }
195 |
196 | return $result;
197 | }
198 |
199 | /**
200 | * {@inheritdoc}
201 | */
202 | public function setMultiple($values, $ttl = null)
203 | {
204 | $this->assertIterable($values, 'values not iterable');
205 |
206 | $success = true;
207 |
208 | foreach ($values as $key => $value) {
209 | $success = $this->set(is_int($key) ? (string)$key : $key, $value, $ttl) && $success;
210 | }
211 |
212 | return $success;
213 | }
214 |
215 | /**
216 | * {@inheritdoc}
217 | */
218 | public function deleteMultiple($keys)
219 | {
220 | $this->assertIterable($keys, 'keys not iterable');
221 |
222 | $success = true;
223 |
224 | foreach ($keys as $key) {
225 | $success = $this->delete($key) && $success;
226 | }
227 |
228 | return $success;
229 | }
230 |
231 |
232 | /**
233 | * Get the current time.
234 | *
235 | * @return int
236 | */
237 | protected function currentTimestamp(): int
238 | {
239 | return time();
240 | }
241 |
242 | /**
243 | * Convert TTL to seconds from now
244 | *
245 | * @param null|int|DateInterval $ttl
246 | * @return int|null
247 | * @throws InvalidArgumentException
248 | */
249 | protected function ttlToSeconds($ttl): ?int
250 | {
251 | if (!isset($ttl)) {
252 | return $this->ttl;
253 | }
254 |
255 | if ($ttl instanceof DateInterval) {
256 | $reference = new DateTimeImmutable();
257 | $endTime = $reference->add($ttl);
258 |
259 | $ttl = $endTime->getTimestamp() - $reference->getTimestamp();
260 | }
261 |
262 | if (!is_int($ttl)) {
263 | $type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl);
264 | throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type");
265 | }
266 |
267 | return isset($this->ttl) ? min($ttl, $this->ttl) : $ttl;
268 | }
269 |
270 | /**
271 | * Convert TTL to epoch timestamp
272 | *
273 | * @param null|int|DateInterval $ttl
274 | * @return int|null
275 | * @throws InvalidArgumentException
276 | */
277 | protected function ttlToTimestamp($ttl): ?int
278 | {
279 | if (!isset($ttl)) {
280 | return isset($this->ttl) ? time() + $this->ttl : null;
281 | }
282 |
283 | if (is_int($ttl)) {
284 | return time() + (isset($this->ttl) ? min($ttl, $this->ttl) : $ttl);
285 | }
286 |
287 | if ($ttl instanceof DateInterval) {
288 | $timestamp = (new DateTimeImmutable())->add($ttl)->getTimestamp();
289 |
290 | return isset($this->ttl) ? min($timestamp, time() + $this->ttl) : $timestamp;
291 | }
292 |
293 | $type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl);
294 | throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type");
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/src/Mysqli.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Arnold Daniels
12 | */
13 |
14 | declare(strict_types=1);
15 |
16 | namespace Desarrolla2\Cache;
17 |
18 | use Desarrolla2\Cache\Option\InitializeTrait;
19 | use mysqli as Server;
20 | use Desarrolla2\Cache\Packer\PackerInterface;
21 | use Desarrolla2\Cache\Packer\SerializePacker;
22 |
23 | /**
24 | * Mysqli cache adapter.
25 | *
26 | * Errors are silently ignored but exceptions are **not** caught. Beware when using `mysqli_report()` to throw a
27 | * `mysqli_sql_exception` on error.
28 | */
29 | class Mysqli extends AbstractCache
30 | {
31 | use InitializeTrait;
32 |
33 | /**
34 | * @var Server
35 | */
36 | protected $server;
37 |
38 | /**
39 | * @var string
40 | */
41 | protected $table = 'cache';
42 |
43 |
44 | /**
45 | * Class constructor
46 | *
47 | * @param Server $server
48 | */
49 | public function __construct(Server $server)
50 | {
51 | $this->server = $server;
52 | }
53 |
54 |
55 | /**
56 | * Initialize table.
57 | * Automatically delete old cache.
58 | */
59 | protected function initialize(): void
60 | {
61 | if ($this->initialized !== false) {
62 | return;
63 | }
64 |
65 | $this->query(
66 | "CREATE TABLE IF NOT EXISTS `{table}` "
67 | . "( `key` VARCHAR(255), `value` BLOB, `ttl` BIGINT UNSIGNED, PRIMARY KEY (`key`) )"
68 | );
69 |
70 | $this->query(
71 | "CREATE EVENT IF NOT EXISTS `apply_ttl_{$this->table}` ON SCHEDULE EVERY 1 HOUR DO BEGIN"
72 | . " DELETE FROM {table} WHERE `ttl` < UNIX_TIMESTAMP();"
73 | . " END"
74 | );
75 |
76 | $this->initialized = true;
77 | }
78 |
79 | /**
80 | * Create the default packer for this cache implementation.
81 | *
82 | * @return PackerInterface
83 | */
84 | protected static function createDefaultPacker(): PackerInterface
85 | {
86 | return new SerializePacker();
87 | }
88 |
89 |
90 | /**
91 | * Set the table name
92 | *
93 | * @param string $table
94 | */
95 | public function setTableOption(string $table)
96 | {
97 | $this->table = $table;
98 | $this->requireInitialization();
99 | }
100 |
101 | /**
102 | * Get the table name
103 | *
104 | * @return string
105 | */
106 | public function getTableOption(): string
107 | {
108 | return $this->table;
109 | }
110 |
111 |
112 | /**
113 | * {@inheritdoc}
114 | */
115 | public function get($key, $default = null)
116 | {
117 | $this->initialize();
118 |
119 | $result = $this->query(
120 | 'SELECT `value` FROM {table} WHERE `key` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
121 | 'si',
122 | $this->keyToId($key),
123 | $this->currentTimestamp()
124 | );
125 |
126 | $row = $result !== false ? $result->fetch_row() : null;
127 |
128 | return $row ? $this->unpack($row[0]) : $default;
129 | }
130 |
131 | /**
132 | * {@inheritdoc}
133 | */
134 | public function getMultiple($keys, $default = null)
135 | {
136 | $idKeyPairs = $this->mapKeysToIds($keys);
137 |
138 | if (empty($idKeyPairs)) {
139 | return [];
140 | }
141 |
142 | $this->initialize();
143 |
144 | $values = array_fill_keys(array_values($idKeyPairs), $default);
145 |
146 | $placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
147 | $paramTypes = str_repeat('s', count($idKeyPairs)) . 'i';
148 | $params = array_keys($idKeyPairs);
149 | $params[] = $this->currentTimestamp();
150 |
151 | $result = $this->query(
152 | "SELECT `key`, `value` FROM {table} WHERE `key` IN ($placeholders) AND (`ttl` > ? OR `ttl` IS NULL)",
153 | $paramTypes,
154 | ...$params
155 | );
156 |
157 | while (([$id, $value] = $result->fetch_row())) {
158 | $key = $idKeyPairs[$id];
159 | $values[$key] = $this->unpack($value);
160 | }
161 |
162 | return $values;
163 | }
164 |
165 | /**
166 | * {@inheritdoc}
167 | */
168 | public function has($key)
169 | {
170 | $this->initialize();
171 |
172 | $result = $this->query(
173 | 'SELECT COUNT(`key`) FROM {table} WHERE `key` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
174 | 'si',
175 | $this->keyToId($key),
176 | $this->currentTimestamp()
177 | );
178 |
179 | [$count] = $result ? $result->fetch_row() : [null];
180 |
181 | return isset($count) && $count > 0;
182 | }
183 |
184 | /**
185 | * {@inheritdoc}
186 | */
187 | public function set($key, $value, $ttl = null)
188 | {
189 | $this->initialize();
190 |
191 | $result = $this->query(
192 | 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES (?, ?, ?)',
193 | 'ssi',
194 | $this->keyToId($key),
195 | $this->pack($value),
196 | $this->ttlToTimestamp($ttl)
197 | );
198 |
199 | return $result !== false;
200 | }
201 |
202 | /**
203 | * {@inheritdoc}
204 | */
205 | public function setMultiple($values, $ttl = null)
206 | {
207 | $this->assertIterable($values, 'values not iterable');
208 |
209 | if (empty($values)) {
210 | return true;
211 | }
212 |
213 | $this->initialize();
214 |
215 | $count = 0;
216 | $params = [];
217 | $timeTtl = $this->ttlToTimestamp($ttl);
218 |
219 | foreach ($values as $key => $value) {
220 | $count++;
221 | $params[] = $this->keyToId(is_int($key) ? (string)$key : $key);
222 | $params[] = $this->pack($value);
223 | $params[] = $timeTtl;
224 | }
225 |
226 | $query = 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES '
227 | . rtrim(str_repeat('(?, ?, ?), ', $count), ', ');
228 |
229 | return (bool)$this->query($query, str_repeat('ssi', $count), ...$params);
230 | }
231 |
232 | /**
233 | * {@inheritdoc}
234 | */
235 | public function delete($key)
236 | {
237 | $this->initialize();
238 |
239 | return (bool)$this->query(
240 | 'DELETE FROM {table} WHERE `key` = ?',
241 | 's',
242 | $this->keyToId($key)
243 | );
244 | }
245 |
246 | /**
247 | * {@inheritdoc}
248 | */
249 | public function deleteMultiple($keys)
250 | {
251 | $idKeyPairs = $this->mapKeysToIds($keys);
252 |
253 | if (empty($idKeyPairs)) {
254 | return true;
255 | }
256 |
257 | $this->initialize();
258 |
259 | $placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
260 | $paramTypes = str_repeat('s', count($idKeyPairs));
261 |
262 | return (bool)$this->query(
263 | "DELETE FROM {table} WHERE `key` IN ($placeholders)",
264 | $paramTypes,
265 | ...array_keys($idKeyPairs)
266 | );
267 | }
268 |
269 | /**
270 | * {@inheritdoc}
271 | */
272 | public function clear()
273 | {
274 | $this->initialize();
275 | return (bool)$this->query('TRUNCATE {table}');
276 | }
277 |
278 |
279 | /**
280 | * Query the MySQL server
281 | *
282 | * @param string $query
283 | * @param string $types
284 | * @param mixed[] $params
285 | * @return \mysqli_result|bool
286 | */
287 | protected function query($query, $types = '', ...$params)
288 | {
289 | $sql = str_replace('{table}', $this->table, $query);
290 |
291 | if ($params === []) {
292 | $ret = $this->server->query($sql);
293 | } else {
294 | $statement = $this->server->prepare($sql);
295 |
296 | if ($statement !== false) {
297 | $statement->bind_param($types, ...$params);
298 |
299 | $ret = $statement->execute();
300 | $ret = $ret ? ($statement->get_result() ?: true) : false;
301 | } else {
302 | $ret = false;
303 | }
304 | }
305 |
306 | if ($this->server->error) {
307 | trigger_error($this->server->error . " $sql", E_USER_NOTICE);
308 | }
309 |
310 | return $ret;
311 | }
312 | }
313 |
--------------------------------------------------------------------------------