├── .formatter.yml ├── .github └── workflows │ └── php.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── build.xml ├── composer.json ├── docs ├── implementations │ ├── apcu.md │ ├── chain.md │ ├── file.md │ ├── memcached.md │ ├── memory.md │ ├── mongodb.md │ ├── mysqli.md │ ├── notcache.md │ ├── phpfile.md │ ├── predis.md │ └── redis.md └── performance.md ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── AbstractCache.php ├── AbstractFile.php ├── Apcu.php ├── CacheInterface.php ├── Chain.php ├── Exception │ ├── BadMethodCallException.php │ ├── CacheException.php │ ├── InvalidArgumentException.php │ └── UnexpectedValueException.php ├── File.php ├── File │ ├── BasicFilename.php │ └── TrieFilename.php ├── Memcached.php ├── Memory.php ├── MongoDB.php ├── Mysqli.php ├── NotCache.php ├── Option │ ├── FilenameTrait.php │ ├── InitializeTrait.php │ ├── PrefixTrait.php │ └── TtlTrait.php ├── Packer │ ├── JsonPacker.php │ ├── MongoDBBinaryPacker.php │ ├── NopPacker.php │ ├── PackerInterface.php │ ├── PackingTrait.php │ └── SerializePacker.php ├── PhpFile.php ├── Predis.php └── Redis.php └── tests ├── AbstractCacheTest.php ├── ApcuCacheTest.php ├── ChainTest.php ├── FileTest.php ├── FileTrieTest.php ├── FileTtlFileTest.php ├── MemcachedTest.php ├── MemoryTest.php ├── MongoDBTest.php ├── MysqliTest.php ├── NotCacheTest.php ├── PhpFileTest.php ├── PredisTest.php ├── RedisTest.php └── performance ├── Apc.php ├── File.php ├── Mongo.php ├── NoCache.php └── common.php /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /composer.lock 3 | /tests/config.json 4 | /vendor 5 | /.idea 6 | /TODO.md 7 | *~ 8 | *.swp 9 | /.phpunit.result.cache -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![life-is-hard-cache-is](https://user-images.githubusercontent.com/100821/41566888-ecd60cde-735d-11e8-893f-da42b2cd65e7.jpg) 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 | [![Daniel González](https://avatars1.githubusercontent.com/u/661529?v=3&s=80)](https://github.com/desarrolla2) 147 | Twitter: [@desarrolla2](https://twitter.com/desarrolla2)\ 148 | [![Arnold Daniels](https://avatars3.githubusercontent.com/u/100821?v=3&s=80)](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 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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` -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 3 3 | paths: 4 | - src 5 | reportUnmatchedIgnoredErrors: false 6 | ignoreErrors: 7 | - /^Variable property access/ 8 | 9 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/MemcachedTest.php: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------