├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── composer.json ├── src ├── AbstractCache.php ├── AbstractPdo.php ├── Adapter.php ├── Apc.php ├── Apcu.php ├── Directory.php ├── Exception.php ├── Factory.php ├── Files.php ├── Indexer │ ├── AbstractIndexer.php │ ├── Adapter.php │ └── MemcachedIndexer.php ├── Memcached.php ├── Mongo.php ├── Mongo │ ├── CollectionAdapter.php │ └── DatabaseAdapter.php ├── Pdo │ ├── Mysql.php │ ├── Pgsql.php │ ├── Sql1999.php │ └── Sqlite.php ├── PsrCache │ ├── InvalidArgumentException.php │ ├── Item.php │ ├── Pool.php │ ├── TaggableItem.php │ └── TaggablePool.php ├── Redis.php ├── Runtime.php └── Serializer │ ├── Adapter.php │ ├── Igbinary.php │ ├── Json.php │ ├── Msgpack.php │ ├── None.php │ ├── Php.php │ └── Stringset.php └── tests ├── AbstractCacheTest.php ├── ApcTest.php ├── ApcuTest.php ├── DirectoryTest.php ├── FactoryTest.php ├── FilesTest.php ├── GenericTestCase.php ├── Indexer ├── ApcIndexerTest_OFF.php ├── GenericIndexerTestCase.php └── MemcachedIndexerTest.php ├── MemcachedTest.php ├── MongoTest.php ├── PdoTest.php ├── PsrCache ├── ItemTest.php ├── PoolTest.php ├── TaggableItemTest.php └── TaggablePoolTest.php ├── RedisTest.php ├── RuntimeTest.php ├── Serializer ├── IgbinaryTest.php ├── JsonTest.php ├── MsgpackTest.php ├── NoneTest.php ├── PhpTest.php ├── StringsetTest.php └── TestCase.php ├── TestCase.php └── bin └── travis-init.sh /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # APIx Cache changelog 2 | 3 | #### Version 1.3.6 (XX-Dec-2020) 4 | - On PHP >= 7.3, if the key passed to `loadKey()` isn't in the cache a 5 | notice is generated because value returned from `PDO::fetch()` is false 6 | but it is treated as an array (see #40 and PR #44 by @BlairCooper). 7 | 8 | #### Version 1.3.5 (11-Jun-2020) 9 | - Improved the `flush` method for the SQL backends (see #40 and PR #39 by @BlairCooper). 10 | 11 | #### Version 1.3.4 (30-Oct-2019) 12 | - Fixed Memcached unit tests for PHP 7.x (PR #39 by @BlairCooper). 13 | - Fixed `Memcached.php::setSerializer` for PHP 7.3 compatibility (PR #37 by @BlairCooper & PR #38 by @mgmbh). 14 | - Fixed Travis builds with PHP 5.* (PR #37 by @BlairCooper). 15 | - Fixed Travis on PHP 5.4 and 5.5 cannot run on Xenial (PR #37 by @BlairCooper). 16 | - Fixed typo in composer suggested package (PR #35 by @Great-Antique). 17 | - Added cas_token to `getTtl()` method (PR #34 contrib by @BlairCooper). 18 | - Added check of "expire" when entry is read from a file (PR #33 contrib by @dimasikturbo). 19 | 20 | #### Version 1.3.3 (18-May-2018) 21 | - Modified `Redis::flush` to delete all keys only from the current DB, instead of deleting all keys from the server (PR #31 contrib by @alexpica). 22 | - Fixes to handle changes with phpredis >= 4, cast return value to Boolean. 23 | - Fix `APCu` adapter to use either `\APCUIterator` or `\APCIterator`. 24 | - Modified Travis config to handle PHP 5.3 and Precise distro. 25 | 26 | #### Version 1.3.2 (19-Jul-2017) 27 | - Added a dedicated `APCu` backend (+ relevant tests) as the extension no longer ship with the backward compatibility module 'apcu-bc’ (see #29). 28 | 29 | #### Version 1.3.1 (19-Jun-2017) 30 | - Fix a MySQL issue where same key/value returned 0 number of updated rows and triggered "SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry" (PR #28 contrib by @dimasikturbo). 31 | 32 | #### Version 1.3.0 (2-May-2017) 33 | - Fix a PSR-6 expiration issue. 34 | - Added `PsrCache\Item::__toString` method to simplify cached value output. 35 | - Added `PsrCache\Pool::__destruct()` method to (garbage collect) magically commit all deferred cached items. 36 | - Removed all deprecated methods from `PsrCache`. 37 | - Various fixes, more unit-tests and some cleanup. 38 | 39 | #### Version 1.2.9 (5-Jan-2017) 40 | - Fix `Files::clean` and `Directory::clean` return to early if failing to find a tag (PR #17 by @melloc01 + relevant tests PR #24). 41 | - Fix `Files::flush(true)` the implementation to flush all was missing (PR #25 contrib by @alexpica). 42 | - Updated to allow patches from php-fig/cache (PR #26 contrib by @vaibhavpandeyvpz). 43 | - Fix to a deprecated method `PsrCache::setExpiration` (PR #27 contrib by @damianopetrungaro). 44 | 45 | #### Version 1.2.8 (28-Oct-2016) 46 | - Added new `mongodb` extension for PHP 5.4 and higher (contrib by @dimasikturbo) which also supports HHVM 3.9 and higher. The legacy `mongo` extension is still provided for PHP 5.6 and lower. 47 | - Set Travis to skip `mongodb` on HHVM (compilation issue). 48 | - Fix array serialisation of nested keys with Mongo (contrib by @dimasikturbo). 49 | 50 | #### Version 1.2.7 (20-July-2016) 51 | - Fix the HHVM issues. 52 | - Fix APC/APCu for both PHP7 and HHVM. 53 | - Updated `.travis` (optimisations). 54 | - Added `msgpack` to Redis, Memcached and to all the PDO backends. 55 | - Added 'auto' and 'json_array' to Memcached. 56 | - Changed Memcached default serializer to `auto`. 57 | - Updated `README.md`. 58 | - Added some additional unit-tests. 59 | - Fix issue #15 "Files cache not correctly handling EOL on Windows" (thanks goes to @davybatsalle). 60 | 61 | #### Version 1.2.6 (4-July-2016) 62 | - Fix issue #13 "TaggablePool and Pool overrides prefix_key and prefix_tag options with hardcoded value" (thanks goes to @alexpica). 63 | - Fix PHP 5.3, using `array()` instead of the short array syntax `[]`. 64 | - Marcked as depreciated `isSerialized()` and `testIsSerialized()`. 65 | - Added `msgpack` serializer. 66 | - Set Travis to skip `Memcached` on PHP 7.0 (not yet officially supported). 67 | - Added additional unit-tests, aiming for 100% code coverage. 68 | 69 | #### Version 1.2.5 (20-Jun-2016) 70 | - Fix issue #12 by adding `Files` and `Directory` backends to the Factory class (thanks goes to @alexpica). 71 | - Added some additional Factory tests. 72 | 73 | #### Version 1.2.4 (6-Jan-2016) 74 | - Updated PSR-Cache (Draft) to PSR-6 (Accepted). 75 | - Marked as deprecated: `PsrCache::setExpiration`, `PsrCache::isRegenerating`, `PsrCache::exists`. 76 | - Added additional unit tests to cover PSR-6. 77 | - Updated `composer.json`. 78 | - Updated `README.md`. 79 | - Updated `.gitignore`. 80 | - Added file locking option to the filesystem backends (contrib by @MacFJA). 81 | 82 | #### Version 1.2.3 (5-Jan-2016) 83 | - Fix APCu versions (contrib by @mimmi20). 84 | - Added `Files` and `Directory` backends (contrib by @MacFJA). 85 | - Updated `README.md`. 86 | 87 | #### Version 1.2.2 (1-Sept-2015) 88 | - Added a `CHANGELOG.md` file. 89 | - Updated PHPUnit to 4.8 version. 90 | - Dropped (partially) PHP 5.3 support - Memcached seems to be broken. 91 | - Dropped PEAR support. 92 | - Refactored `.travis.yml` tests. 93 | - Made Travis faster (using Docker containers and skipping allowable failures). 94 | - Added support to PHP 7.0. 95 | 96 | #### Version 1.2.1 (4-Oct-2014) 97 | - Added setOption(). 98 | - Updated `composer.json`. 99 | - Updated `README.md`. 100 | - Added Scrutinizer checks. 101 | - Merged Scrutinizer Auto-Fixes. 102 | - Various minor changes. 103 | 104 | #### Version 1.2.0 (19-Sept-2014) 105 | - Added preflight option to PDO backends. 106 | - Added PSR-6 Cache support as a factory class. 107 | - Added [Coverall](https://coveralls.io/github/frqnck/apix-cache) support. 108 | - Updated `README.md`. 109 | - Added APCu support. 110 | - Added PHP 5.6 and HHVM support. 111 | - Updated the `README.md`. 112 | 113 | #### Version 1.1.0 (30-Jan-2013) 114 | - Added `--prefer-source` (recommended by @seldaek). 115 | - Updated the `README.md`. 116 | - Added some unit tests. 117 | - Added JSON support to Redis. 118 | 119 | #### Version 1.0.5 (23-Jan-2013) 120 | - Added `loadKey()`, `loadTag()` and removed `load()`. 121 | 122 | #### Version 1.0.4 (22-Jan-2013) 123 | - Added dedicated SQL1999 class. 124 | - Fixed PDO and SQL definitions. 125 | - Fixed `.travis.yml`. 126 | 127 | #### Version 1.0.3 (20-Jan-2013) 128 | - Added some aditional tests. 129 | - Refactored Serialisers. 130 | 131 | #### Version 1.0.2 (20-Jan-2013) 132 | - Added MongoDB implementation. 133 | - Various fixes. 134 | 135 | #### Version 1.0.1 (15-Jan-2013) 136 | - Fixed test for Redis with igBinary. 137 | - Added APC and PhpRedis environments. 138 | - Added PHP 5.5 support. 139 | - Fixed `.travis.yml`. 140 | - Added additional tests and minor changes. 141 | - Updated `README.md`. 142 | 143 | #### Version 1.0.0 (11-Jan-2013) 144 | - Initial release. 145 | 146 |
147 |   _|_|    _|_|    _|     _|      _|
148 | _|    _| _|    _|         _|    _|
149 | _|    _| _|    _| _|        _|_|
150 | _|_|_|_| _|_|_|   _| _|_|   _|_|
151 | _|    _| _|       _|      _|    _|
152 | _|    _| _|       _|     _|      _|
153 | 
154 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | New BSD License 2 | =============== 3 | 4 | Copyright (c) 2010-2016, Franck Cassedanne, OUARZ Technology Ltd. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the names of the copyright holders nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Apix Cache, cache-tagging for PHP 2 | [![Build Status](https://travis-ci.org/apix/cache.svg?branch=master)](https://travis-ci.org/apix/cache) 3 | ================================= 4 | [![Latest Stable Version](https://poser.pugx.org/apix/cache/v/stable.svg)](https://packagist.org/packages/apix/cache) 5 | [![Total Downloads](https://poser.pugx.org/apix/cache/downloads)](https://packagist.org/packages/apix/cache) 6 | [![Build Status](https://scrutinizer-ci.com/g/frqnck/apix-cache/badges/build.png?b=master)](https://scrutinizer-ci.com/g/frqnck/apix-cache/build-status/master) 7 | [![Code Quality](https://scrutinizer-ci.com/g/frqnck/apix-cache/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/frqnck/apix-cache/?branch=master) 8 | [![Code Coverage](https://scrutinizer-ci.com/g/frqnck/apix-cache/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/frqnck/apix-cache/?branch=master) 9 | [![License](https://poser.pugx.org/apix/cache/license.svg)](https://packagist.org/packages/apix/cache) 10 | 11 | Apix Cache is a generic and thin cache wrapper with a PSR-6 interface to various caching backends and emphasising cache **tagging** and **indexing**. 12 | 13 | > Cache-tagging allows to find/update all data items with one or more given tags. Providing, for instance, a batch delete of all obsolete entries matching a speficic tag such as a version string. 14 | 15 | * Fully **unit-tested** and compliant with PSR-1, PSR-2, PSR-4 and **PSR-6** (Cache). 16 | * [Continuously integrated](https://travis-ci.org/apix/cache) 17 | * against **PHP** ~~5.3~~, **5.4**, **5.5**, **5.6**, **7.0**, **7.1**, **7.2**, **7.3** ~~and HHVM~~, 18 | * and against `APC`, `APCu`, `Redis`, `MongoDB`, `Sqlite`, `MySQL`, `PgSQL` and `Memcached`, ... 19 | * supports a range of serializers: `igBinary`, `msgpack`, `json`, `php`, ... 20 | * Extendable, additional extensions are available: 21 | * **[apix/simple-cache](//github.com/apix/simple-cache)** provides a SimpleCache (PSR-16) interface. 22 | * More contributions will be linked here. 23 | * Available as a [Composer](https://packagist.org/packages/apix/cache) ~~and as a [PEAR](http://pear.ouarz.net)~~ package. 24 | 25 | ⇄ *[Pull requests](//github.com/frqnck/apix-cache/blob/master/.github/CONTRIBUTING.md)* and ★ *Stars* are always welcome. For bugs and feature request, please [create an issue](//github.com/frqnck/apix-cache/issues/new). 26 | 27 | --- 28 | 29 | Cache backends 30 | -------------- 31 | Currently, the following cache store are supplied: 32 | 33 | * **[`APCu`](http://php.net/apcu)** and **[APC](http://php.net/apc)** *with tagging support*, 34 | * **[Redis](#redis-specific)** using the [`PhpRedis`](https://github.com/phpredis/phpredis) extension *with tagging support*, 35 | * **[MongoDB](#mongodb-specific)** using either the new [`mongodb`](http://php.net/mongodb) or the legacy [`mongo`](http://php.net/mongo) extension *with tagging support*, 36 | * **[Memcached](#memcached-specific)** using the [`Memcached`](http://php.net/book.memcached.php) extension *with indexing, tagging and namespacing support*, 37 | * and relational databases using **[PDO](#pdo-specific)** *with tagging support*: 38 | * Dedicated drivers for **[SQLite](http://www.sqlite.org)**, **[PostgreSQL](http://www.postgresql.org)** and **[MySQL](http://www.mysql.com)** (also works with Amazon Aurora, MariaDB and Percona), 39 | * A generic **[Sql1999](https://en.wikipedia.org/wiki/SQL:1999)** driver for [4D](http://www.4d.com/), [Cubrid](http://www.cubrid.org), [SQL Server](http://www.microsoft.com/sqlserver), [Sybase](http://www.sybase.com), [Firebird](http://www.firebirdsql.org), [ODBC](https://en.wikipedia.org/wiki/Open_Database_Connectivity), [Interbase](http://www.embarcadero.com/products/interbase), [IBM DB2](www.ibm.com/software/data/db2/), [IDS](http://www-01.ibm.com/software/data/informix/), [Oracle](http://www.oracle.com/database)... 40 | * **[Directory](#filesystem-specific)** and **[Files](#filesystem-specific)** based *with tagging support*, 41 | * **Runtime**, in-memory array storage. 42 | 43 | Factory usage (PSR-Cache wrapper) 44 | ------------- 45 | 46 | ```php 47 | use Apix\Cache; 48 | 49 | $backend = new \Redis(); 50 | #$backend = new \PDO('sqlite:...'); // Any supported client object e.g. Memcached, MongoClient, ... 51 | #$backend = new Cache\Files($options); // or one that implements Apix\Cache\Adapter 52 | #$backend = 'apcu'; // or an adapter name (string) e.g. "APC", "Runtime" 53 | #$backend = new MyArrayObject(); // or even a plain array() or \ArrayObject. 54 | 55 | $cache = Cache\Factory::getPool($backend); // without tagging support 56 | #$cache = Cache\Factory::getTaggablePool($backend); // with tagging 57 | 58 | $item = $cache->getItem('wibble_id'); 59 | 60 | if ( !$cache->isHit() ) { 61 | $data = compute_expensive_stuff(); 62 | $item->set($data); 63 | $cache->save($item); 64 | } 65 | 66 | return $item->get(); 67 | ``` 68 | 69 | Basic usage (Apix native) 70 | ----------- 71 | 72 | ```php 73 | use Apix\Cache; 74 | 75 | $cache = new Cache\Apcu; 76 | 77 | // try to retrieve 'wibble_id' from the cache 78 | if ( !$data = $cache->load('wibble_id') ) { 79 | 80 | // otherwise, get some data from the origin 81 | // example of arbitrary mixed data 82 | $data = array('foo' => 'bar'); 83 | 84 | // and save it to the cache 85 | $cache->save($data, 'wibble_id'); 86 | } 87 | ``` 88 | You can also use the folowing in your use cases: 89 | ```php 90 | // save $data to the cache as 'wobble_id', 91 | // tagging it along the way as 'baz' and 'flob', 92 | // and set the ttl to 300 seconds (5 minutes) 93 | $cache->save($data, 'wobble_id', array('baz', 'flob'), 300); 94 | 95 | // retrieve all the cache ids under the tag 'baz' 96 | $ids = $cache->loadTag('baz'); 97 | 98 | // clear out all items with a 'baz' tag 99 | $cache->clean('baz'); 100 | 101 | // remove the named item 102 | $cache->delete('wibble_id'); 103 | 104 | // flush out the cache (of all -your- stored items) 105 | $cache->flush(); 106 | ``` 107 | 108 | Advanced usage 109 | -------------- 110 | ### Options shared by all the backends 111 | ```php 112 | use Apix\Cache; 113 | 114 | // default options 115 | $options = array( 116 | 'prefix_key' => 'apix-cache-key:', // prefix cache keys 117 | 'prefix_tag' => 'apix-cache-tag:', // prefix cache tags 118 | 'tag_enable' => true // wether to enable tags support 119 | ); 120 | 121 | // start APCu as a local cache 122 | $local_cache = new Cache\Apcu($options); 123 | ``` 124 | 125 | ### Redis specific 126 | ```php 127 | // additional (default) options 128 | $options['atomicity'] = false; // false is faster, true is guaranteed 129 | $options['serializer'] = 'php'; // null, php, igbinary, json and msgpack 130 | 131 | $redis_client = new \Redis; // instantiate phpredis* 132 | $distributed_cache = new Cache\Redis($redis_client, $options); 133 | ``` 134 | \* see [PhpRedis](https://github.com/nicolasff/phpredis) for instantiation usage. 135 | 136 | ### Memcached specific 137 | ```php 138 | // additional (default) options, specific to Memcached 139 | $options['prefix_key'] = 'key_'; // prefix cache keys 140 | $options['prefix_tag'] = 'tag_'; // prefix cache tags 141 | $options['prefix_idx'] = 'idx_'; // prefix cache indexes 142 | $options['prefix_nsp'] = 'nsp_'; // prefix cache namespaces 143 | $options['serializer'] = 'auto'; // auto, igbinary, msgpack, php, json and json_array. 144 | 145 | $memcached = new \Memcached; // a Memcached*** instance 146 | $shared_cache = new Cache\Memcached($memcached, $options); 147 | ``` 148 | 149 | The serialzer `auto` (default) is `igBinary` if available, then `msgpack` if available, then `php` otherwise. 150 | 151 | \*\*\* see [Memcached](http://php.net/manual/en/book.memcached.php) for instantiation details. 152 | 153 | ### MongoDB specific 154 | ```php 155 | // additional (default) options 156 | $options['object_serializer'] = 'php'; // null, json, php, igBinary 157 | $options['db_name'] = 'apix'; // name of the mongo db 158 | $options['collection_name'] = 'cache'; // name of the mongo collection 159 | 160 | $mongo = new \MongoDB\Client; // MongoDB native driver 161 | #$mongo = new \MongoClient; // or MongoDB legacy driver 162 | $cache = new Cache\Mongo($mongo, $options); 163 | ``` 164 | 165 | ### PDO specific 166 | ```php 167 | // additional (default) options, specific to the PDO backends 168 | $options['db_table'] = 'cache'; // table to hold the cache 169 | $options['serializer'] = 'php'; // null, php, igbinary, json and msgpack 170 | $options['preflight'] = true; // wether to preflight the DB 171 | $options['timestamp'] = 'Y-m-d H:i:s'; // the timestamp DB format 172 | 173 | // with SQLITE 174 | $dbh = new \PDO('sqlite:/tmp/apix_tests.sqlite3'); 175 | $relational_cache = new Cache\Pdo\Sqlite($dbh, $options); 176 | 177 | // with MYSQL, MariaDB and Percona 178 | $dbh = new \PDO('mysql:host=xxx;port=xxx;dbname=xxx', 'user', 'pass'); 179 | $mysql_cache = new Cache\Pdo\Mysql($dbh, $options); 180 | 181 | // with PGSQL 182 | $dbh = new \PDO('pgsql:dbname=xxx;host=xxx', 'xxx', 'xxx'); 183 | $postgres_cache = new Cache\Pdo\Pgsql($dbh, $options); 184 | 185 | // with a SQL:1999 compliant DB, e.g. Oracle 186 | $dbh = new \PDO('oci:dbname=xxx', 'xxx', 'xxx'); 187 | $sql1999_cache = new Cache\Pdo\Sql1999($dbh, $options); 188 | ``` 189 | The `preflight` option will create on-the-fly the required tables if these are mssing. 190 | Once these tables exist, set `preflight` to `false` in order to avoid the extraneous checks. 191 | 192 | ### Filesystem specific 193 | 194 | ```php 195 | // additional (default) options 196 | $options['directory'] = sys_get_temp_dir() . '/apix-cache'; // Directory where the cache is created 197 | $options['locking'] = true; // File locking (recommended) 198 | 199 | $files_cache = new Cache\Files($options); 200 | // or 201 | $directory_cache = new Cache\Directory($options); 202 | ``` 203 | 204 | - **Files**: the cache metadata (expiration time and tags) are stored in the cache file directly. 205 | - **Directory**: the metadata are stored separately from the cached data. 206 | 207 | Installation 208 | ------------------------ 209 | 210 | This project adheres to [Semantic Versioning](http://semver.org/) and can be installed using composer: 211 | 212 | $ composer require apix/cache:^1.3 213 | 214 | All notable changes to this project are documented in its [CHANGELOG](CHANGELOG.md). 215 | 216 | License 217 | ------- 218 | This work is licensed under the New BSD license -- see the [LICENSE](LICENSE.txt) for the full details.
Copyright (c) 2010-2016 Franck Cassedanne 219 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apix/cache", 3 | "description": "A thin PSR-6 cache wrapper with a generic interface to various caching backends emphasising cache taggging and indexing to Redis, Memcached, PDO/SQL, APC and other adapters.", 4 | "type": "library", 5 | "keywords": ["apc", "APCu", "redis", "mongodb", "mongo", "igbinary", "serializer", "json", "pdo", "sqlite", "mysql", "postgres", "pgsql", "memcached", "memcache", "psrCache", "psr-6", "psr-cache", "Filesystem", "session", "cache", "caching", "msgpack", "tagging"], 6 | "homepage": "https://github.com/frqnck/apix-cache", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Franck Cassedanne", 11 | "email": "franck@ouarz.net" 12 | }, 13 | { 14 | "name": "Apix Cache Community", 15 | "homepage": "https://github.com/frqnck/apix-cache/contributors" 16 | } 17 | ], 18 | "support": { 19 | "irc": "irc://irc.freenode.org/ouarz" 20 | }, 21 | "require": { 22 | "php": ">=5.3.0", 23 | "psr/cache": "^1.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^4.0|^5.0", 27 | "php-coveralls/php-coveralls": "~1.1" 28 | }, 29 | "suggest": { 30 | "ext-apc": "Allows to cache into APC data store ~ up to PHP 5.4.", 31 | "ext-apcu": "Allows to use APCu userland caching ~ any PHP version.", 32 | "ext-redis": "So you can cache into Redis servers.", 33 | "ext-mongo": "Allows to cache into MongoDB instances using http://php.net/mongo ~ up to PHP 5.6.", 34 | "ext-mongodb": "Allows to cache into MongoDB instances using https://php.net/mongodb ~ from PHP 7 and above and HHVM.", 35 | "ext-memcached": "Allows to use Memcached distributed memory caching system", 36 | "ext-pdo": "Allows to cache into PDO supported DBs such as Oracle, MS SQL server, IBM DB2.", 37 | "ext-pdo_sqlite": "Allows to cache into SQLite.", 38 | "ext-pdo_mysql": "Allows to use MySQL.", 39 | "ext-pdo_pgsql": "Allows to use PostgreSQL", 40 | "ext-igbinary": "Fast and small serialization format", 41 | "msgpack/msgpack-php": "MessagePack serialization format" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Apix\\Cache\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Apix\\Cache\\tests\\": "tests/" 51 | } 52 | }, 53 | "provide": { 54 | "psr/cache-implementation": "^1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AbstractCache.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * Base class provides the cache wrappers structure. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | abstract class AbstractCache implements Adapter 21 | { 22 | 23 | /** 24 | * Holds an injected adapter. 25 | * @var object 26 | */ 27 | protected $adapter = null; 28 | 29 | /** 30 | * @var Serializer\Adapter 31 | */ 32 | protected $serializer; 33 | 34 | /** 35 | * Holds some generic default options. 36 | * @var array 37 | */ 38 | protected $options = array( 39 | 'prefix_key' => 'apix-cache-key:', // prefix cache keys 40 | 'prefix_tag' => 'apix-cache-tag:', // prefix cache tags 41 | 'tag_enable' => true // wether to enable tagging 42 | ); 43 | 44 | /** 45 | * Constructor use to set the adapter and dedicated options. 46 | * 47 | * @param object|null $adapter The adapter to set, generally an object. 48 | * @param array|null $options The array of user options. 49 | */ 50 | public function __construct($adapter=null, array $options=null) 51 | { 52 | $this->adapter = $adapter; 53 | $this->setOptions($options); 54 | } 55 | 56 | /** 57 | * Sets and merges the options (overriding the default options). 58 | * 59 | * @param array|null $options The array of user options. 60 | */ 61 | public function setOptions(array $options=null) 62 | { 63 | if (null !== $options) { 64 | $this->options = $options+$this->options; 65 | } 66 | } 67 | 68 | /** 69 | * Returns the named option. 70 | * 71 | * @param string $key 72 | * @return mixed 73 | */ 74 | public function getOption($key) 75 | { 76 | if (!isset($this->options[$key])) { 77 | throw new PsrCache\InvalidArgumentException( 78 | sprintf('Invalid option "%s"', $key) 79 | ); 80 | } 81 | 82 | return $this->options[$key]; 83 | } 84 | 85 | /** 86 | * Sets the named option. 87 | * 88 | * @param string $key 89 | * @param mixed $value 90 | */ 91 | public function setOption($key, $value) 92 | { 93 | return $this->options[$key] = $value; 94 | } 95 | 96 | /** 97 | * Returns a prefixed and sanitased cache id. 98 | * 99 | * @param string $key The base key to prefix. 100 | * @return string 101 | */ 102 | public function mapKey($key) 103 | { 104 | return $this->sanitise($this->options['prefix_key'] . $key); 105 | } 106 | 107 | /** 108 | * Returns a prefixed and sanitased cache tag. 109 | * 110 | * @param string $tag The base tag to prefix. 111 | * @return string 112 | */ 113 | public function mapTag($tag) 114 | { 115 | return $this->sanitise($this->options['prefix_tag'] . $tag); 116 | } 117 | 118 | /** 119 | * Returns a sanitased string for keying/tagging purpose. 120 | * 121 | * @param string $key The string to sanitise. 122 | * @return string 123 | */ 124 | public function sanitise($key) 125 | { 126 | return $key; 127 | // return str_replace(array('/', '\\', ' '), '_', $key); 128 | } 129 | 130 | /** 131 | * Gets the injected adapter. 132 | * 133 | * @return object 134 | */ 135 | public function getAdapter() 136 | { 137 | return $this->adapter; 138 | } 139 | 140 | /** 141 | * Sets the serializer. 142 | * 143 | * @param string $name 144 | */ 145 | public function setSerializer($name) 146 | { 147 | if (null === $name) { 148 | $this->serializer = null; 149 | } else { 150 | $classname = __NAMESPACE__ . '\Serializer\\'; 151 | $classname .= ucfirst(strtolower($name)); 152 | $this->serializer = new $classname(); 153 | } 154 | } 155 | 156 | /** 157 | * Gets the serializer. 158 | * 159 | * @return Serializer\Adapter 160 | */ 161 | public function getSerializer() 162 | { 163 | return $this->serializer; 164 | } 165 | 166 | /** 167 | * Retrieves the cache content for the given key or the keys for a given tag. 168 | * 169 | * @param string $key The cache id to retrieve. 170 | * @param string $type The type of the key (either 'key' or 'tag'). 171 | * @return mixed|null Returns the cached data or null if not set. 172 | */ 173 | public function load($key, $type='key') 174 | { 175 | return $type == 'key' ? $this->loadKey($key) : $this->loadTag($key); 176 | } 177 | 178 | /** 179 | * Returns the given string without the given prefix. 180 | * 181 | * @param string $str The subject string 182 | * @param string $prefix The prefix to remove 183 | * @return string 184 | */ 185 | public function removePrefix($str, $prefix) 186 | { 187 | return substr($str, 0, strlen($prefix)) == $prefix 188 | ? substr($str, strlen($prefix)) 189 | : $str; 190 | } 191 | 192 | /** 193 | * Returns the given string without the internal key prefix. 194 | * 195 | * @param string $str 196 | * @return string 197 | */ 198 | public function removePrefixKey($str) 199 | { 200 | return $this->removePrefix($str, $this->options['prefix_key']); 201 | } 202 | 203 | /** 204 | * Returns the given string without the internal tag prefix. 205 | * 206 | * @param string $str 207 | * @return string 208 | */ 209 | public function removePrefixTag($str) 210 | { 211 | return $this->removePrefix($str, $this->options['prefix_tag']); 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /src/AbstractPdo.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * PDO cache wrapper with tag support. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | abstract class AbstractPdo extends AbstractCache 21 | { 22 | 23 | /** 24 | * Holds the SQL definitions. 25 | * @var array 26 | */ 27 | protected $sql_definitions; 28 | 29 | /** 30 | * Holds the array of TTLs. 31 | * @var array 32 | */ 33 | protected $ttls = array(); 34 | 35 | /** 36 | * Constructor. 37 | * 38 | * @param \PDO $pdo An instance of a PDO class. 39 | * @param array $options Array of options. 40 | */ 41 | public function __construct(\PDO $pdo, array $options=null) 42 | { 43 | // default options 44 | $this->options['db_table'] = 'cache'; // table to hold the cache 45 | $this->options['serializer'] = 'php'; // null, php, igBinary, json 46 | // and msgpack 47 | $this->options['preflight'] = true; // wether to preflight the DB 48 | $this->options['timestamp'] = 'Y-m-d H:i:s'; // timestamp db format 49 | 50 | $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 51 | 52 | parent::__construct($pdo, $options); 53 | $this->setSerializer($this->options['serializer']); 54 | 55 | if ($this->options['preflight']) { 56 | $this->initDb(); 57 | } 58 | } 59 | 60 | /** 61 | * Initialises the database and its indexes (if required, non-destructive). 62 | * 63 | * @return self Provides a fluent interface 64 | */ 65 | public function initDb() 66 | { 67 | $this->adapter->exec( $this->getSql('init') ); 68 | 69 | $this->createIndexTable('key_idx'); 70 | $this->createIndexTable('exp_idx'); 71 | $this->createIndexTable('tag_idx'); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Creates the specified indexe table (if missing). 78 | * 79 | * @param string $index 80 | * @return boolean 81 | */ 82 | public function createIndexTable($index) 83 | { 84 | if (!isset($this->sql_definitions[$index])) { 85 | return false; 86 | } 87 | 88 | try { 89 | $this->adapter->exec($this->getSql($index, $this->options['db_table'])); 90 | } catch (\PDOException $ex) { 91 | if (1061 !== $ex->errorInfo[1]) { 92 | // if not "Index already exists" 93 | } 94 | } 95 | 96 | return $this->adapter->errorCode() == '00000'; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function loadKey($key) 103 | { 104 | $result = null; 105 | 106 | $key = $this->mapKey($key); 107 | $sql = $this->getSql('loadKey'); 108 | $values = array('key' => $key, 'now' => time()); 109 | 110 | $cached = $this->exec($sql, $values)->fetch(); 111 | 112 | if (false !== $cached) { 113 | $this->ttls[$key] = $cached['expire']; 114 | 115 | if (null !== $cached['data'] && null !== $this->serializer) { 116 | $result = $this->serializer->unserialize($cached['data']); 117 | } 118 | else { 119 | $result = $cached['data']; 120 | } 121 | } 122 | 123 | return $result; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function loadTag($tag) 130 | { 131 | $sql = $this->getSql('loadTag'); 132 | // $tag = $this->mapTag($tag); 133 | $values = array('tag' => "%$tag%", 'now' => time()); 134 | 135 | $items = $this->exec($sql, $values)->fetchAll(); 136 | 137 | $keys = array(); 138 | foreach ($items as $item) { 139 | $keys[] = $item['key']; 140 | } 141 | 142 | return empty($keys) ? null : $keys; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function save($data, $key, array $tags=null, $ttl=null) 149 | { 150 | $key = $this->mapKey($key); 151 | $values = array( 152 | 'key' => $key, 153 | 'data' => null !== $this->serializer 154 | ? $this->serializer->serialize($data) 155 | : $data, 156 | 'exp' => null !== $ttl && 0 !== $ttl ? time()+$ttl : null, 157 | 'dated' => $this->getTimestamp() 158 | ); 159 | 160 | $this->ttls[$key] = $values['exp']; 161 | 162 | $values['tags'] = $this->options['tag_enable'] && null !== $tags 163 | ? implode(', ', $tags) 164 | : null; 165 | 166 | // upsert 167 | $sql = $this->getSql('update'); 168 | $nb = $this->exec($sql, $values)->rowCount(); 169 | if ($nb == 0) { 170 | $sql = $this->getSql('insert'); 171 | $nb = $this->exec($sql, $values)->rowCount(); 172 | } 173 | 174 | return (boolean) $nb; 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function delete($key) 181 | { 182 | $sql = $this->getSql('delete'); 183 | $values = array($this->mapKey($key)); 184 | 185 | return (boolean) $this->exec($sql, $values)->rowCount(); 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function clean(array $tags) 192 | { 193 | $values = array(); 194 | foreach ($tags as $tag) { 195 | // $tag = $this->mapTag($tag); 196 | $values[] = '%' . $tag . '%'; 197 | } 198 | 199 | $sql = $this->getSql( 200 | 'clean', implode(' OR ', array_fill( 201 | 0, count($tags), $this->getSql('clean_like'))) 202 | ); 203 | 204 | return (boolean) $this->exec($sql, $values)->rowCount(); 205 | } 206 | 207 | /** 208 | * {@inheritdoc} 209 | */ 210 | public function flush($all=false) 211 | { 212 | if (true === $all) { 213 | return false !== $this->adapter->exec($this->getSql('flush_all')); 214 | } 215 | 216 | $value = array( 217 | $this->options['prefix_key'].'%', 218 | $this->options['prefix_tag'].'%' 219 | ); 220 | 221 | return (boolean) $this->exec($this->getSql('flush'), $value)->rowCount(); 222 | } 223 | 224 | /** 225 | * Purges expired items. 226 | * 227 | * @param integer|null $add Extra time in second to add. 228 | * @return boolean Returns True on success or False on failure. 229 | */ 230 | public function purge($add=null) 231 | { 232 | $time = null == $add ? time() : time()+$add; 233 | 234 | return (boolean) $this->adapter->exec($this->getSql('purge', $time)); 235 | } 236 | 237 | /** 238 | * Gets the named SQL definition. 239 | * 240 | * @param string $key 241 | * @param string|integer $value An additional value. 242 | * @return string 243 | */ 244 | protected function getSql($key, $value=null) 245 | { 246 | return sprintf( 247 | $this->sql_definitions[$key], 248 | $this->options['db_table'], 249 | $value 250 | ); 251 | } 252 | 253 | /** 254 | * Prepares and executes a SQL query. 255 | * 256 | * @param string $sql The SQL to prepare. 257 | * @param array $values The values to execute. 258 | * @return \PDOStatement Provides a fluent interface 259 | */ 260 | protected function exec($sql, array $values) 261 | { 262 | $prep = $this->adapter->prepare($sql); 263 | $prep->execute($values); 264 | 265 | return $prep; 266 | } 267 | 268 | /** 269 | * Returns the current driver's name. 270 | * 271 | * @param \PDO $pdo An instance of a PDO class. 272 | * @return string Either 'Mysql', 'Pgsql', 'Sqlite' or 'Sql1999'. 273 | */ 274 | public static function getDriverName(\PDO $pdo) 275 | { 276 | $name = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 277 | if (!in_array($name, array('sqlite', 'mysql', 'pgsql'))) { 278 | // @codeCoverageIgnoreStart 279 | // sql1999 specific tests are run on Sqlite. 280 | $name = 'sql1999'; 281 | } 282 | // @codeCoverageIgnoreEnd 283 | 284 | return ucfirst($name); 285 | } 286 | 287 | /** 288 | * Returns a formated timestamp. 289 | * 290 | * @param integer|null $time If null, use the current time. 291 | */ 292 | public function getTimestamp($time=null) 293 | { 294 | return date( 295 | $this->options['timestamp'], 296 | null != $time ? $time : time() 297 | ); 298 | } 299 | 300 | /** 301 | * {@inheritdoc} 302 | */ 303 | public function getTtl($key) 304 | { 305 | $mKey = $this->mapKey($key); 306 | 307 | return isset($this->ttls[$mKey]) 308 | ? $this->ttls[$mKey]-time() 309 | : false; 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /src/Adapter.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * The interface/adapter that the cache wrappers must implement. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | interface Adapter 21 | { 22 | 23 | /** 24 | * Retrieves the cache content for the given key. 25 | * 26 | * @param string $key The cache key to retrieve. 27 | * @return mixed|null Returns the cached data or null. 28 | */ 29 | public function loadKey($key); 30 | 31 | /** 32 | * Retrieves the cache keys for the given tag. 33 | * 34 | * @param string $tag The cache tag to retrieve. 35 | * @return array|null Returns an array of cache keys or null. 36 | */ 37 | public function loadTag($tag); 38 | 39 | /** 40 | * Saves data to the cache. 41 | * 42 | * @param mixed $data The data to cache. 43 | * @param string $key The cache id to save. 44 | * @param array $tags The cache tags for this cache entry. 45 | * @param int $ttl The time-to-live in seconds, if set to null the 46 | * cache is valid forever. 47 | * @return boolean Returns True on success or False on failure. 48 | */ 49 | public function save($data, $key, array $tags=null, $ttl=null); 50 | 51 | /** 52 | * Deletes the specified cache record. 53 | * 54 | * @param string $key The cache id to remove. 55 | * @return boolean Returns True on success or False on failure. 56 | */ 57 | public function delete($key); 58 | 59 | /** 60 | * Removes all the cached entries associated with the given tag names. 61 | * 62 | * @param array $tags The array of tags to remove. 63 | * @return boolean Returns True on success or False on failure. 64 | */ 65 | public function clean(array $tags); 66 | 67 | /** 68 | * Flush all the cached entries. 69 | * 70 | * @param boolean $all Wether to flush the whole database, or (preferably) 71 | * the entries prefixed with prefix_key and prefix_tag. 72 | * @return boolean Returns True on success or False on failure. 73 | */ 74 | public function flush($all=false); 75 | 76 | /** 77 | * Returns the time-to-live (in seconds) for the given key. 78 | * 79 | * @param string $key The name of the key. 80 | * @return int|false Returns the number of seconds left, 0 if valid 81 | * forever or False if the key is non-existant. 82 | */ 83 | public function getTtl($key); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Apc.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * APC Cache and APCu (User-Cache) wrapper with emulated tag support. 17 | * 18 | * @package Apix\Cache 19 | * @author Franck Cassedanne 20 | */ 21 | class Apc extends AbstractCache 22 | { 23 | 24 | /** 25 | * Constructor. 26 | */ 27 | public function __construct(array $options = array()) 28 | { 29 | parent::__construct(null, $options); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function loadKey($key) 36 | { 37 | return $this->get($this->mapKey($key)); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function loadTag($tag) 44 | { 45 | return $this->get($this->mapTag($tag)); 46 | // TODO: return $this->getIndex($this->mapTag($tag))->load(); 47 | } 48 | 49 | /** 50 | * Retrieves the cache item for the given id. 51 | * 52 | * @param string $id The cache id to retrieve. 53 | * @param boolean $success The variable to store the success value. 54 | * @return mixed|null Returns the cached data or null. 55 | */ 56 | public function get($id, $success = null) 57 | { 58 | $cached = apc_fetch($id, $success); 59 | 60 | return false === $success ? null : $cached; 61 | } 62 | 63 | /** 64 | * Returns the named indexer. 65 | * 66 | * @param string $name The name of the index. 67 | * @return Indexer\Adapter 68 | */ 69 | // public function getIndex($name) 70 | // { 71 | // return new Indexer\ApcIndexer($name, $this); 72 | // } 73 | 74 | /** 75 | * {@inheritdoc} 76 | * 77 | * APC does not support natively cache-tags so we simulate them. 78 | */ 79 | public function save($data, $key, array $tags = null, $ttl = null) 80 | { 81 | $key = $this->mapKey($key); 82 | $store = array($key => $data); 83 | 84 | if ($this->options['tag_enable'] && !empty($tags)) { 85 | 86 | // add all the tags to the index key. 87 | // TODO: $this->getIndex($key)->add($tags); 88 | 89 | foreach ($tags as $tag) { 90 | $tag = $this->mapTag($tag); 91 | $keys = apc_fetch($tag, $success); 92 | if (false === $success) { 93 | $store[$tag] = array($key); 94 | } else { 95 | $keys[] = $key; 96 | $store[$tag] = array_unique($keys); 97 | } 98 | } 99 | } 100 | 101 | return !in_array(false, apc_store($store, null, $ttl)); 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | * 107 | * APC does not support natively cache-tags so we simulate them. 108 | */ 109 | public function delete($key) 110 | { 111 | $key = $this->mapKey($key); 112 | 113 | if ( ($success = apc_delete($key)) && $this->options['tag_enable']) { 114 | 115 | $iterator = $this->getIterator( 116 | '/^' . preg_quote($this->options['prefix_tag']) . '/', 117 | \APC_ITER_VALUE 118 | ); 119 | foreach ($iterator as $tag => $keys) { 120 | if ( false !== ($k = array_search($key, $keys['value'])) ) { 121 | unset($keys['value'][$k]); 122 | if (empty($keys['value'])) { 123 | apc_delete($tag); 124 | } else { 125 | apc_store($tag, $keys['value']); 126 | } 127 | } 128 | continue; 129 | } 130 | } 131 | 132 | return $success; 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | * 138 | * APC does not support natively cache-tags so we simulate them. 139 | */ 140 | public function clean(array $tags) 141 | { 142 | $rmed = array(); 143 | foreach ($tags as $tag) { 144 | $tag = $this->mapTag($tag); 145 | $keys = apc_fetch($tag, $success); 146 | if ($success) { 147 | foreach ($keys as $key) { 148 | $rmed[] = apc_delete($key); 149 | } 150 | $rmed[] = apc_delete($tag); 151 | } else { 152 | $rmed[] = false; 153 | } 154 | } 155 | 156 | return !in_array(false, $rmed); 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | * 162 | * APC does not support natively cache-tags so we simulate them. 163 | */ 164 | public function flush($all = false) 165 | { 166 | if (true === $all) { 167 | return apc_clear_cache('user'); 168 | } 169 | 170 | $iterator = $this->getIterator( 171 | '/^' . preg_quote($this->options['prefix_key']) 172 | .'|' . preg_quote($this->options['prefix_tag']) . '/', 173 | \APC_ITER_KEY 174 | ); 175 | 176 | $rmed = array(); 177 | foreach ($iterator as $key => $data) { 178 | $rmed[] = apc_delete($key); 179 | } 180 | 181 | return empty($rmed) || in_array(false, $rmed) ? false : true; 182 | } 183 | 184 | /** 185 | * Returns an APC iterator. 186 | * 187 | * @param string $search 188 | * @param integer $format 189 | * @return \APCIterator 190 | */ 191 | protected function getIterator($search = null, $format = \APC_ITER_ALL) 192 | { 193 | return new \APCIterator('user', $search, $format, 100, \APC_LIST_ACTIVE); 194 | } 195 | 196 | /** 197 | * Returns some internal informations about a APC cached item. 198 | * 199 | * @param string $key 200 | * @return array|false 201 | */ 202 | public function getInternalInfos($key) 203 | { 204 | $iterator = $this->getIterator( 205 | '/^' . preg_quote($this->options['prefix_key']) . '/', 206 | \APC_ITER_KEY | \APC_ITER_VALUE | \APC_ITER_TTL 207 | ); 208 | 209 | $key = $this->mapKey($key); 210 | foreach ($iterator as $k => $v) { 211 | if ($k != $key) 212 | continue; 213 | 214 | return $v; 215 | } 216 | 217 | return false; 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function getTtl($key) 224 | { 225 | $info = $this->getInternalInfos($key); 226 | if ( $info && isset($info['ttl']) ) { 227 | return $info['ttl']; 228 | // return $info['ttl'] > 0 ? $info['creation_time']+$info['ttl'] : 0; 229 | } 230 | 231 | return false; 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /src/Apcu.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * APCu Cache(User-Cache) wrapper with emulated tag support. 17 | * 18 | * @package Apix\Cache 19 | * @author Franck Cassedanne 20 | */ 21 | class Apcu extends Apc 22 | { 23 | 24 | /** 25 | * Retrieves the cache item for the given id. 26 | * 27 | * @param string $id The cache id to retrieve. 28 | * @param boolean $success The variable to store the success value. 29 | * @return mixed|null Returns the cached data or null. 30 | */ 31 | public function get($id, $success = null) 32 | { 33 | $cached = apcu_fetch($id, $success); 34 | 35 | return false === $success ? null : $cached; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * APC does not support natively cache-tags so we simulate them. 42 | */ 43 | public function save($data, $key, array $tags = null, $ttl = null) 44 | { 45 | $key = $this->mapKey($key); 46 | $store = array($key => $data); 47 | 48 | if ($this->options['tag_enable'] && !empty($tags)) { 49 | 50 | // add all the tags to the index key. 51 | // TODO: $this->getIndex($key)->add($tags); 52 | 53 | foreach ($tags as $tag) { 54 | $tag = $this->mapTag($tag); 55 | $keys = apcu_fetch($tag, $success); 56 | if (false === $success) { 57 | $store[$tag] = array($key); 58 | } else { 59 | $keys[] = $key; 60 | $store[$tag] = array_unique($keys); 61 | } 62 | } 63 | } 64 | 65 | return !in_array(false, apcu_store($store, null, $ttl)); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | * 71 | * APC does not support natively cache-tags so we simulate them. 72 | */ 73 | public function delete($key) 74 | { 75 | $key = $this->mapKey($key); 76 | 77 | if (($success = apcu_delete($key)) && $this->options['tag_enable']) { 78 | $iterator = $this->getIterator( 79 | '/^' . preg_quote($this->options['prefix_tag']) . '/', 80 | \APC_ITER_VALUE 81 | ); 82 | foreach ($iterator as $tag => $keys) { 83 | if (false !== ($k = array_search($key, $keys['value']))) { 84 | unset($keys['value'][$k]); 85 | if (empty($keys['value'])) { 86 | apcu_delete($tag); 87 | } else { 88 | apcu_store($tag, $keys['value']); 89 | } 90 | } 91 | continue; 92 | } 93 | } 94 | 95 | return $success; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | * 101 | * APC does not support natively cache-tags so we simulate them. 102 | */ 103 | public function clean(array $tags) 104 | { 105 | $rmed = array(); 106 | foreach ($tags as $tag) { 107 | $tag = $this->mapTag($tag); 108 | $keys = apcu_fetch($tag, $success); 109 | if ($success) { 110 | foreach ($keys as $key) { 111 | $rmed[] = apcu_delete($key); 112 | } 113 | $rmed[] = apcu_delete($tag); 114 | } else { 115 | $rmed[] = false; 116 | } 117 | } 118 | 119 | return !in_array(false, $rmed); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | * 125 | * APC does not support natively cache-tags so we simulate them. 126 | */ 127 | public function flush($all = false) 128 | { 129 | if (true === $all) { 130 | return apcu_clear_cache(); 131 | } 132 | 133 | $iterator = $this->getIterator( 134 | '/^' . preg_quote($this->options['prefix_key']) 135 | .'|' . preg_quote($this->options['prefix_tag']) . '/', 136 | \APC_ITER_KEY 137 | ); 138 | 139 | $rmed = array(); 140 | foreach ($iterator as $key => $data) { 141 | $rmed[] = apcu_delete($key); 142 | } 143 | 144 | return empty($rmed) || in_array(false, $rmed) ? false : true; 145 | } 146 | 147 | /** 148 | * Returns an APC iterator. 149 | * 150 | * @param string $search 151 | * @param integer $format 152 | * @return \APCIterator 153 | */ 154 | protected function getIterator($search = null, $format = \APC_ITER_ALL) 155 | { 156 | return class_exists('\APCUIterator') 157 | ? new \APCUIterator($search, $format, 100, \APC_LIST_ACTIVE) 158 | : new \APCIterator('user', $search, $format, 100, \APC_LIST_ACTIVE); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 9 | * 10 | */ 11 | 12 | namespace Apix\Cache; 13 | 14 | /** 15 | * Main Cache Exception. 16 | */ 17 | class Exception extends \Exception implements \Psr\Cache\CacheException { } 18 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 9 | * 10 | */ 11 | 12 | namespace Apix\Cache; 13 | 14 | use Apix\Cache; 15 | 16 | /** 17 | * Apix Cache Factory class provides a PSR-Cache wrapper. 18 | * 19 | * @author Franck Cassedanne 20 | */ 21 | class Factory 22 | { 23 | 24 | const ERROR = '%s requires either a supported client object e.g. "\Redis", "\MongoClient", ...; or an object that implements Apix\Cache\Adapter; or an adapter name (string) e.g. "APC", "array"; or even an a plain array().'; 25 | 26 | /** 27 | * Holds an array of supported cache clients. 28 | * @var array 29 | */ 30 | public static $clients = array( 31 | 'Runtime', 'Array', 'ArrayObject', 'Apc', 'Apcu', 32 | 'Redis', 'MongoClient', 'Memcached', 'PDO', 33 | 'Files', 'Directory' 34 | ); 35 | 36 | /** 37 | * Holds an associative array of cache adapters. 38 | * @var array 39 | */ 40 | public static $adapters = array( 41 | 'MongoClient' => 'Mongo', 42 | 'PDO' => 'Pdo', 43 | 'ArrayObject' => 'Runtime' 44 | ); 45 | 46 | /** 47 | * Factory pattern. 48 | * 49 | * @param mixed $mix Either a supported client object e.g. '\Redis'; 50 | * or one that implements \Apix\Cache\Adapter; 51 | * or an adapter name (string) e.g. "APC", "Runtime"; 52 | * or even a plain array() or \ArrayObject. 53 | * @param array $options An array of options 54 | * @param boolean $taggable Wether to return a taggable pool. 55 | * @return PsrCache\Pool|PsrCache\TaggablePool 56 | * @throws PsrCache\InvalidArgumentException 57 | * @throws Apix\Cache\Exception 58 | */ 59 | public static function getPool($mix, array $options=array(), $taggable=false) 60 | { 61 | switch (true) { 62 | 63 | case is_a($mix, 'Apix\Cache\Adapter'): 64 | $class = $mix; 65 | $mix = null; 66 | break; 67 | 68 | case is_object($mix) 69 | && in_array($name = get_class($mix), self::$clients): 70 | 71 | if ($name == 'PDO') { 72 | $name = 'Pdo\\' . AbstractPdo::getDriverName($mix); 73 | } else { 74 | $name = isset(self::$adapters[$name]) 75 | ? self::$adapters[$name] 76 | : $name; 77 | } 78 | break; 79 | 80 | case is_string($mix) 81 | && in_array( 82 | $name = strtolower($mix), 83 | $clients = array_map('strtolower', self::$clients) 84 | ): 85 | $key = array_search($name, $clients); 86 | $name = self::$clients[$key]; 87 | $name = $name == 'Array' || $name == 'ArrayObject' ? 'Runtime' : $name; 88 | $mix = null; 89 | break; 90 | 91 | case is_array($mix): 92 | $name = 'Runtime'; 93 | break; 94 | 95 | default: 96 | throw new PsrCache\InvalidArgumentException( 97 | sprintf(self::ERROR, __CLASS__) 98 | ); 99 | } 100 | 101 | if (!isset($class)) { 102 | $class = '\Apix\Cache\\' . $name; 103 | } 104 | 105 | $cache = null === $mix 106 | ? new $class($options) 107 | : new $class($mix, $options); 108 | 109 | return $taggable 110 | ? new PsrCache\TaggablePool($cache) 111 | : new PsrCache\Pool($cache); 112 | } 113 | 114 | /** 115 | * @see self::getPool 116 | * @return PsrCache\TaggablePool 117 | */ 118 | public static function getTaggablePool($mix, array $options=array()) 119 | { 120 | return self::getPool($mix, $options, true); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/Files.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 9 | * 10 | */ 11 | 12 | namespace Apix\Cache; 13 | 14 | /** 15 | * Class Files 16 | * In files cache wrapper. 17 | * Expiration time and tags are stored in the cache file 18 | * 19 | * @package Apix\Cache 20 | * @author MacFJA 21 | */ 22 | class Files extends AbstractCache 23 | { 24 | /** 25 | * Constructor. 26 | * 27 | * @param array $options Array of options. 28 | */ 29 | public function __construct(array $options = array()) 30 | { 31 | $options += array( 32 | 'directory' => sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'apix-cache', 33 | 'locking' => true 34 | ); 35 | parent::__construct(null, $options); 36 | if (!file_exists($this->getOption('directory')) || !is_dir($this->getOption('directory'))) { 37 | mkdir($this->getOption('directory'), 0755, true); 38 | } 39 | } 40 | 41 | /** 42 | * Retrieves the cache content for the given key. 43 | * 44 | * @param string $key The cache key to retrieve. 45 | * @return mixed|null Returns the cached data or null. 46 | */ 47 | public function loadKey($key) 48 | { 49 | $key = $this->mapKey($key); 50 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . base64_encode($key); 51 | 52 | if (!file_exists($path) || !is_file($path)) { 53 | return null; 54 | } 55 | 56 | $data = $this->readFile($path); 57 | 58 | if ('' === $data) { 59 | unlink($path); 60 | return null; 61 | } 62 | $pos0 = strpos($data, PHP_EOL, 0); 63 | if (false === $pos0) {// Un-complete file 64 | unlink($path); 65 | return null; 66 | } 67 | 68 | $eolLen = strlen(PHP_EOL); 69 | $pos = strpos($data, PHP_EOL, $pos0+$eolLen); 70 | if (false === $pos) {// Un-complete file 71 | unlink($path); 72 | return null; 73 | } 74 | 75 | $expire = (int)substr($data, $pos0 + $eolLen, $pos - $pos0 - $eolLen); 76 | if ($expire != 0 && ($expire - time() < 0)) { // expired 77 | unlink($path); 78 | return null; 79 | } 80 | 81 | $serialized = substr($data, $pos+$eolLen); 82 | return unserialize($serialized); 83 | } 84 | 85 | /** 86 | * Retrieves the cache keys for the given tag. 87 | * 88 | * @param string $tag The cache tag to retrieve. 89 | * @return array|null Returns an array of cache keys or null. 90 | */ 91 | public function loadTag($tag) 92 | { 93 | if (!$this->getOption('tag_enable')) { 94 | return null; 95 | } 96 | 97 | $encoded = base64_encode($this->mapTag($tag)); 98 | $found = array(); 99 | $files = scandir($this->getOption('directory')); 100 | foreach ($files as $file) { 101 | if (substr($file, 0, 1) === '.') { 102 | continue; 103 | } 104 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . $file; 105 | $fileTags = explode(' ', $this->readFile($path, 1)); 106 | 107 | if (in_array($encoded, $fileTags, true)) { 108 | $found[] = base64_decode($file); 109 | } 110 | } 111 | 112 | if (0 === count($found)) { 113 | return null; 114 | } 115 | return $found; 116 | } 117 | 118 | /** 119 | * Get the file data. 120 | * If enable, lock file to preserve atomicity 121 | * 122 | * @param string $path The file path 123 | * @param int $line The line to read. If -1 read the whole file 124 | * @return string 125 | */ 126 | protected function readFile($path, $line = -1) 127 | { 128 | $handle = fopen($path, 'r'); 129 | if ($this->getOption('locking')) { 130 | flock($handle, LOCK_SH); 131 | } 132 | 133 | if (-1 === $line) { 134 | $data = stream_get_contents($handle); 135 | } else { 136 | for ($read = 1; $read < $line; $read++) { 137 | fgets($handle); 138 | } 139 | $data = rtrim(fgets($handle), PHP_EOL); 140 | } 141 | 142 | if ($this->getOption('locking')) { 143 | flock($handle, LOCK_UN); 144 | } 145 | fclose($handle); 146 | 147 | return $data; 148 | } 149 | 150 | /** 151 | * Saves data to the cache. 152 | * 153 | * @param mixed $data The data to cache. 154 | * @param string $key The cache id to save. 155 | * @param array $tags The cache tags for this cache entry. 156 | * @param int $ttl The time-to-live in seconds, if set to null the 157 | * cache is valid forever. 158 | * @return boolean Returns True on success or False on failure. 159 | */ 160 | public function save($data, $key, array $tags = null, $ttl = null) 161 | { 162 | $key = $this->mapKey($key); 163 | $expire = (null === $ttl) ? 0 : time() + $ttl; 164 | 165 | $tag = ''; 166 | if (null !== $tags) { 167 | $baseTags = $tags; 168 | array_walk($baseTags, function (&$item, $key, Files $cache) { 169 | $item = base64_encode($cache->mapTag($item)); 170 | }, $this); 171 | $tag = implode(' ', $baseTags); 172 | } 173 | 174 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . base64_encode($key); 175 | file_put_contents( 176 | $path, 177 | $tag . PHP_EOL . $expire . PHP_EOL . serialize($data), 178 | $this->getOption('locking') ? LOCK_EX : null 179 | ); 180 | return true; 181 | } 182 | 183 | /** 184 | * Deletes the specified cache record. 185 | * 186 | * @param string $key The cache id to remove. 187 | * @return boolean Returns True on success or False on failure. 188 | */ 189 | public function delete($key) 190 | { 191 | $key = $this->mapKey($key); 192 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . base64_encode($key); 193 | if (!file_exists($path)) { 194 | return false; 195 | } 196 | 197 | return unlink($path); 198 | } 199 | 200 | /** 201 | * Removes all the cached entries associated with the given tag names. 202 | * 203 | * @param array $tags The array of tags to remove. 204 | * @return boolean Returns True on success or False on failure. 205 | */ 206 | public function clean(array $tags) 207 | { 208 | $toRemove = array(); 209 | $cleaned = false; 210 | 211 | foreach ($tags as $tag) { 212 | $keys = $this->loadTag($tag); 213 | if (null === $keys) { 214 | continue; 215 | } 216 | $cleaned = true; 217 | $toRemove = array_merge($toRemove, $keys); 218 | } 219 | $toRemove = array_unique($toRemove); 220 | 221 | foreach ($toRemove as $key) { 222 | $this->delete($this->removePrefixKey($key)); 223 | } 224 | 225 | return $cleaned; 226 | } 227 | 228 | /** 229 | * Flush all the cached entries. 230 | * 231 | * @param boolean $all Wether to flush the whole database, or (preferably) 232 | * the entries prefixed with prefix_key and prefix_tag. 233 | * @return boolean Returns True on success or False on failure. 234 | */ 235 | public function flush($all = false) 236 | { 237 | $files = scandir($this->getOption('directory')); 238 | foreach ($files as $file) { 239 | if ('.' === substr($file, 0, 1)) { 240 | continue; 241 | } 242 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . $file; 243 | $fullKey = base64_decode($file); 244 | $key = $this->removePrefixKey($fullKey); 245 | 246 | if ($all || (!$all && ($key !== $fullKey || '' === $this->options['prefix_key']))) { 247 | unlink($path); 248 | } 249 | } 250 | } 251 | 252 | /** 253 | * Returns the time-to-live (in seconds) for the given key. 254 | * 255 | * @param string $key The name of the key. 256 | * @return int|false Returns the number of seconds left, 0 if valid 257 | * forever or False if the key is non-existant. 258 | */ 259 | public function getTtl($key) 260 | { 261 | $key = $this->mapKey($key); 262 | $path = $this->getOption('directory') . DIRECTORY_SEPARATOR . base64_encode($key); 263 | if (!file_exists($path) || !is_file($path)) { 264 | return false; 265 | } 266 | 267 | $expire = $this->readFile($path, 2); 268 | 269 | if (0 === (int) $expire) { 270 | return 0; 271 | } 272 | 273 | return $expire - time(); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/Indexer/AbstractIndexer.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Indexer; 14 | 15 | /** 16 | * Base class provides the cache wrappers structure. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | abstract class AbstractIndexer implements Adapter 21 | { 22 | 23 | /** 24 | * Holds the name of the index. 25 | * @var string 26 | */ 27 | protected $name; 28 | 29 | /** 30 | * Holds the index items. 31 | * @var array 32 | */ 33 | protected $items = array(); 34 | 35 | /** 36 | * Returns the index name. 37 | * 38 | * @return string a string. 39 | */ 40 | public function getName() 41 | { 42 | return $this->name; 43 | } 44 | 45 | /** 46 | * Returns the items index. 47 | * 48 | * @return Returns an array of items or null failure. 49 | */ 50 | public function getItems() 51 | { 52 | return $this->items; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Indexer/Adapter.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Indexer; 14 | 15 | /** 16 | * The interface/adapter that the cache indexers must implement. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | interface Adapter 21 | { 22 | 23 | /** 24 | * Adds one or many element(s) to the index. 25 | * 26 | * @param array|string $elements The element(s) to remove from the index. 27 | * @return Returns TRUE on success or FALSE on failure. 28 | */ 29 | public function add($elements); 30 | 31 | /** 32 | * Removes one or many element(s) from the index. 33 | * 34 | * @param array|string $elements The element(s) to remove from the index. 35 | * @return Returns True on success or False on failure. 36 | */ 37 | public function remove($elements); 38 | 39 | /** 40 | * Returns the indexed items. 41 | * 42 | * @return Returns True on success or False on failure. 43 | */ 44 | public function load(); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Indexer/MemcachedIndexer.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Indexer; 14 | 15 | use Apix\Cache\Memcached; 16 | use Apix\Cache\Serializer; 17 | 18 | /** 19 | * Memcached index. 20 | * 21 | * @author Franck Cassedanne 22 | */ 23 | class MemcachedIndexer extends AbstractIndexer 24 | { 25 | 26 | const DIRTINESS_THRESHOLD = 100; 27 | 28 | /** 29 | * Holds an instane of a serializer. 30 | * @var Serializer\Stringset 31 | */ 32 | protected $serializer; 33 | 34 | /** 35 | * Holds the index store engine. 36 | * @var Memcached 37 | */ 38 | protected $engine; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param string $name 44 | * @param Memcached $engine 45 | */ 46 | public function __construct($name, Memcached $engine) 47 | { 48 | $this->name = $name; 49 | $this->engine = $engine; 50 | 51 | $this->serializer = new Serializer\Stringset(); 52 | } 53 | 54 | /** 55 | * Gets the adapter. 56 | * 57 | * @return \Memcached 58 | */ 59 | public function getAdapter() 60 | { 61 | return $this->engine->getAdapter(); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function add($elements) 68 | { 69 | $str = $this->serializer->serialize((array) $elements); 70 | 71 | $success = $this->getAdapter()->append($this->name, $str); 72 | 73 | if (false === $success) { 74 | $success = $this->getAdapter()->add($this->name, $str); 75 | } 76 | 77 | return (boolean) $success; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function remove($elements) 84 | { 85 | $str = $this->serializer->serialize((array) $elements, '-'); 86 | 87 | return (boolean) $this->getAdapter()->append($this->name, $str); 88 | } 89 | 90 | /** 91 | * Returns the indexed items. 92 | * 93 | * @return boolean Returns True on success or Null on failure. 94 | */ 95 | public function load() 96 | { 97 | // &$cas_token holds the unique 64-bit float token generated 98 | // by memcache for the named item @see \Memcached::get 99 | $str = $this->engine->get($this->name, $cas_token); 100 | 101 | if (null === $str) { 102 | return null; 103 | } 104 | $this->items = $this->serializer->unserialize($str); 105 | 106 | if ($this->serializer->getDirtiness() > self::DIRTINESS_THRESHOLD) { 107 | $this->purge($cas_token); 108 | } 109 | 110 | return $this->items; 111 | } 112 | 113 | /** 114 | * Purge atomically the index. 115 | * 116 | * @param double|null $cas_token 117 | * @return float $cas_token The Memcache CAS token. 118 | */ 119 | protected function purge($cas_token) 120 | { 121 | $str = $this->serializer->serialize($this->items); 122 | 123 | return $this->getAdapter()->cas($cas_token, $this->name, $str); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Mongo.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * Mongo cache wrapper. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | class Mongo extends AbstractCache 21 | { 22 | 23 | /** 24 | * Holds the array of TTLs. 25 | * @var array 26 | */ 27 | protected $ttls = array(); 28 | 29 | /** 30 | * Holds the MongoDB object 31 | * @var \MongoDB|Mongo\DatabaseAdapter 32 | */ 33 | public $db; 34 | 35 | /** 36 | * Holds the MongoCollection object 37 | * @var \MongoCollection|Mongo\CollectionAdapter 38 | */ 39 | public $collection; 40 | 41 | /** 42 | * Indicates the use of the legacy \MongoClient. 43 | * @var bool 44 | */ 45 | private $is_legacy = false; 46 | 47 | /** 48 | * Constructor. Sets the Mongo DB adapter. 49 | * 50 | * @param \MongoClient|\MongoDB\Client $Mongo A Mongo client instance. 51 | * @param array $options Array of options. 52 | */ 53 | public function __construct($Mongo, array $options=null) 54 | { 55 | if (!is_a($Mongo, '\MongoDB\Client') && !is_a($Mongo, '\MongoClient')) { 56 | throw new \InvalidArgumentException( 57 | 'Expected instance of "\MongoDB\Client" or "\MongoClient"' 58 | ); 59 | } 60 | 61 | // default options 62 | $this->options['db_name'] = 'apix'; 63 | $this->options['collection_name'] = 'cache'; 64 | $this->options['object_serializer'] = 'php'; // null, php, json, igBinary. 65 | 66 | // Set the adapter and merge the user+default options 67 | parent::__construct($Mongo, $options); 68 | 69 | if (is_a($Mongo, '\MongoDB\Client')) { 70 | $this->is_legacy = false; 71 | $this->db = new Mongo\DatabaseAdapter( 72 | $this->adapter->{$this->options['db_name']} 73 | ); 74 | } else { 75 | $this->is_legacy = true; 76 | $this->db = $this->adapter->selectDB($this->options['db_name']); 77 | } 78 | 79 | $this->collection = $this->db->createCollection( 80 | $this->options['collection_name'], 81 | array() 82 | ); 83 | 84 | $this->collection->ensureIndex( 85 | array('key' => 1), 86 | array( 87 | 'unique' => true, 88 | 'dropDups' => true, 89 | // 'sparse' => true 90 | ) 91 | ); 92 | 93 | // Using MongoDB TTL collections (MongoDB 2.2+) 94 | $this->collection->ensureIndex( 95 | array('expire' => 1), 96 | array('expireAfterSeconds' => 1) 97 | ); 98 | 99 | $this->setSerializer($this->options['object_serializer']); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function loadKey($key) 106 | { 107 | $mKey = $this->mapKey($key); 108 | $cache = $this->get($mKey); 109 | 110 | // check expiration 111 | if ( null === $cache or ( 112 | isset($cache['expire']) && (string) $cache['expire']->sec < time() 113 | )) { 114 | unset($this->ttls[$mKey]); 115 | 116 | return null; 117 | } 118 | 119 | return $cache['serialized'] 120 | ? $this->serializer->unserialize( 121 | $this->is_legacy 122 | ? $cache['serialized']->bin 123 | : $cache['serialized']->getData()) 124 | : $cache['data']; 125 | } 126 | 127 | /** 128 | * Retrieves the cache item for the given id. 129 | * 130 | * @param string $key The cache key to retrieve. 131 | * @return mixed|null Returns the cached data or null. 132 | */ 133 | public function get($key) 134 | { 135 | $cache = $this->collection->findOne( 136 | array('key' => $key), 137 | array('data', 'expire', 'serialized') 138 | ); 139 | 140 | if ($cache !== null) { 141 | $this->ttls[$key] = isset($cache['expire']) 142 | ? $cache['expire']->sec - time() 143 | : 0; 144 | } 145 | 146 | return $cache; 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | public function loadTag($tag) 153 | { 154 | $cache = $this->collection->find( 155 | array('tags' => $this->mapTag($tag)), 156 | array('key') 157 | ); 158 | 159 | $keys = array_map( 160 | function ($v) { return $v['key']; }, 161 | array_values(iterator_to_array($cache)) 162 | ); 163 | 164 | return empty($keys) ? null : $keys; 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function save($data, $key, array $tags=null, $ttl=null) 171 | { 172 | $key = $this->mapKey($key); 173 | 174 | $cache = array('key' => $key, 'data' => null, 'serialized' => null); 175 | 176 | if (null !== $this->serializer && (is_object($data) || is_array($data))) { 177 | $cache['serialized'] = 178 | $this->is_legacy 179 | ? new \MongoBinData($this->serializer->serialize($data), \MongoBinData::BYTE_ARRAY) 180 | : new \MongoDB\BSON\Binary($this->serializer->serialize($data), \MongoDB\BSON\Binary::TYPE_GENERIC); 181 | } else { 182 | $cache['data'] = $data; 183 | } 184 | 185 | if ($this->options['tag_enable'] && null !== $tags) { 186 | $cache['tags'] = array(); 187 | foreach ($tags as $tag) { 188 | $cache['tags'][] = $this->mapTag($tag); 189 | } 190 | } 191 | 192 | $this->ttls[$key] = 0; 193 | 194 | if (null !== $ttl && 0 !== $ttl) { 195 | $expire = time()+$ttl; 196 | 197 | $cache['expire'] = $this->is_legacy 198 | ? new \MongoDate($expire) 199 | : new \MongoDB\BSON\UTCDateTime($expire * 1000); 200 | 201 | $this->ttls[$key] = $ttl; 202 | } 203 | 204 | $res = $this->collection->update( 205 | array('key' => $key), $cache, array('upsert' => true) 206 | ); 207 | 208 | return (boolean) $res['ok']; 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | public function clean(array $tags) 215 | { 216 | $items = array(); 217 | foreach ($tags as $tag) { 218 | $items += (array) $this->loadTag($tag); 219 | } 220 | $res = $this->collection->remove( 221 | array('key'=>array('$in'=>$items)) 222 | ); 223 | 224 | return (boolean) $res['n']; 225 | } 226 | 227 | /** 228 | * {@inheritdoc} 229 | */ 230 | public function delete($key) 231 | { 232 | $res = $this->collection->remove( 233 | array('key' => $this->mapKey($key)) 234 | ); 235 | 236 | return (boolean) $res['n']; 237 | } 238 | 239 | /** 240 | * {@inheritdoc} 241 | */ 242 | public function flush($all=false) 243 | { 244 | if (true === $all) { 245 | $res = $this->collection->drop(); 246 | return (boolean) $res['ok']; 247 | } 248 | 249 | $regex = $this->is_legacy 250 | ? new \MongoRegex('/^' . $this->mapKey('') . '/') 251 | : array('$regex' => '^' . $this->mapKey('')); 252 | 253 | $res = $this->collection->remove( array('key' => $regex) ); 254 | 255 | return (boolean) $res['ok']; 256 | } 257 | 258 | /** 259 | * Counts the number of cached items. 260 | * 261 | * @return integer Returns the number of items in the cache. 262 | */ 263 | public function count() 264 | { 265 | return (integer) $this->collection->count(); 266 | } 267 | 268 | /** 269 | * {@inheritdoc} 270 | */ 271 | public function getTtl($key) 272 | { 273 | $mKey = $this->mapKey($key); 274 | 275 | return !isset($this->ttls[$mKey]) && null === $this->get($mKey) 276 | ? false 277 | : $this->ttls[$mKey]; 278 | } 279 | 280 | } 281 | -------------------------------------------------------------------------------- /src/Mongo/CollectionAdapter.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 24 | } 25 | 26 | public function ensureIndex(array $keys, array $options = array()) 27 | { 28 | $this->collection->createIndex($keys, $options); 29 | } 30 | 31 | public function insert($a, array $options = array()) 32 | { 33 | try { 34 | $this->collection->insertOne($a, $options); 35 | return ['ok' => 1]; 36 | } catch (\Exception $e) { 37 | return ['ok' => 0, 'error' => $e->getMessage()]; 38 | } 39 | } 40 | 41 | public function update(array $criteria, array $newobj, array $options = array()) 42 | { 43 | try { 44 | $this->collection->updateOne($criteria, array('$set' => $newobj), $options); 45 | return ['ok' => 1]; 46 | } catch (\Exception $e) { 47 | return ['ok' => 0, 'error' => $e->getMessage()]; 48 | } 49 | } 50 | 51 | public function remove(array $criteria = array(), array $options = array()) 52 | { 53 | try { 54 | $result = $this->collection->deleteMany($criteria, $options); 55 | return ['ok' => 1, 'n' => $result->getDeletedCount()]; 56 | } catch (\Exception $e) { 57 | return ['ok' => 0, 'error' => $e->getMessage()]; 58 | } 59 | } 60 | 61 | public function find(array $query = array(), array $fields = array()) 62 | { 63 | $options = ['projection' => array_fill_keys($fields, 1) + ['_id' => 0]]; 64 | 65 | return $this->collection->find($query, $options); 66 | } 67 | 68 | public function findOne(array $query = array(), array $fields = array()) 69 | { 70 | $options = ['projection' => array_fill_keys($fields, 1) + ['_id' => 0]]; 71 | 72 | $result = $this->collection->findOne($query, $options); 73 | 74 | // mimic \MongoDate->sec 75 | if (!empty($result['expire'])) { 76 | $sec = $result['expire']->toDateTime()->getTimestamp(); 77 | $result['expire'] = new \stdClass(); 78 | $result['expire']->sec = $sec; 79 | } 80 | 81 | return $result; 82 | } 83 | 84 | public function drop() 85 | { 86 | $this->collection->drop(); 87 | return ['ok' => 1]; 88 | } 89 | 90 | public function count($query = array()) 91 | { 92 | return $this->collection->count($query); 93 | } 94 | } -------------------------------------------------------------------------------- /src/Mongo/DatabaseAdapter.php: -------------------------------------------------------------------------------- 1 | db = $db; 23 | } 24 | 25 | /** 26 | * @param string $name 27 | * @param array $options 28 | * @return CollectionAdapter 29 | */ 30 | public function createCollection($name, $options) 31 | { 32 | return new CollectionAdapter($this->db->selectCollection($name, $options)); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Pdo/Mysql.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Pdo; 14 | 15 | use Apix\Cache\AbstractPdo; 16 | 17 | /** 18 | * The Mysql (PDO) wrapper. 19 | * 20 | * @author Franck Cassedanne 21 | */ 22 | class Mysql extends AbstractPdo 23 | { 24 | 25 | /** 26 | * Holds the SQL definitions for MySQL 3.x, 4.x and 5.x. 27 | */ 28 | protected $sql_definitions = array( 29 | 'init' => 'CREATE TABLE IF NOT EXISTS %s (`key` VARCHAR(255) NOT NULL, 30 | `data` LONGTEXT NULL, `tags` TEXT NULL, `expire` INTEGER 31 | UNSIGNED, `dated` TIMESTAMP, PRIMARY KEY (`key`)) 32 | ENGINE=MYISAM DEFAULT charset=utf8;', 33 | 'key_idx' => 'CREATE INDEX `%s_key_idx` ON `%s` (`key`);', 34 | 'exp_idx' => 'CREATE INDEX `%s_exp_idx` ON `%s` (`expire`);', 35 | 36 | // 'tag_idx' will throw MYSQL ERROR 1170 -- if the index is needed then 37 | // we should split keys and tags into diff tables and use varchar(255). 38 | // 'tag_idx' => 'CREATE INDEX `%s_tag_idx` ON `%s` (`tags`);', 39 | 40 | 'loadKey' => 'SELECT `data`, `expire` FROM `%s` WHERE `key`=:key 41 | AND (`expire` IS NULL OR `expire` > :now);', 42 | 'loadTag' => 'SELECT `key` FROM `%s` WHERE `tags` LIKE :tag 43 | AND (`expire` IS NULL OR `expire` > :now);', 44 | 'update' => 'UPDATE `%s` SET `data`=:data, `tags`=:tags, 45 | `expire`=:exp, `dated`=:dated WHERE `key`=:key;', 46 | 'insert' => 'INSERT INTO `%s` (`key`, `data`, `tags`, `expire`, 47 | `dated`) VALUES (:key, :data, :tags, :exp, :dated) 48 | ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), 49 | `tags`=VALUES(`tags`), `expire`=VALUES(`expire`);', 50 | 'delete' => 'DELETE FROM `%s` WHERE `key`=?;', 51 | 'clean' => 'DELETE FROM `%s` WHERE %s;', // %s 'clean_like' iterated 52 | 'clean_like'=> 'tags LIKE ?', 53 | 'flush_all' => 'DELETE FROM `%s`;', 54 | 'flush' => 'DELETE FROM `%s` WHERE `key` LIKE ? OR `tags` LIKE ?;', 55 | 'purge' => 'DELETE FROM `%s` WHERE `expire` IS NOT NULL AND `expire` < %d;' 56 | ); 57 | 58 | /** 59 | * Constructor. 60 | * 61 | * @param \PDO $pdo 62 | * @param array $options Array of options. 63 | */ 64 | public function __construct(\PDO $pdo, array $options=null) 65 | { 66 | parent::__construct($pdo, $options); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Pdo/Pgsql.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Pdo; 14 | 15 | use Apix\Cache\AbstractPdo; 16 | 17 | /** 18 | * The PostgreSQL (PDO) wrapper. 19 | * 20 | * @author Franck Cassedanne 21 | */ 22 | class Pgsql extends AbstractPdo 23 | { 24 | 25 | /** 26 | * Holds the SQL definitions for PostgreSQL. 27 | */ 28 | protected $sql_definitions = array( 29 | 'init' => 'CREATE TABLE IF NOT EXISTS "%s" 30 | ("key" VARCHAR PRIMARY KEY, "data" TEXT, "tags" TEXT, 31 | "expire" INTEGER, "dated" TIMESTAMP);', 32 | 'key_idx' => 'CREATE INDEX "%s_key_idx" ON "%s" ("key");', 33 | 'exp_idx' => 'CREATE INDEX "%s_exp_idx" ON "%s" ("expire");', 34 | 'tag_idx' => 'CREATE INDEX "%s_tag_idx" ON "%s" ("tags");', 35 | 'loadKey' => 'SELECT "data", "expire" FROM "%s" WHERE "key"=:key AND 36 | ("expire" IS NULL OR "expire" > :now);', 37 | 'loadTag' => 'SELECT "key" FROM "%s" WHERE "tags" LIKE :tag AND 38 | ("expire" IS NULL OR "expire" > :now);', 39 | 'update' => 'UPDATE "%s" SET "data"=:data, "tags"=:tags, "expire"=:exp, 40 | "dated"=:dated WHERE "key"=:key;', 41 | 'insert' => 'INSERT INTO "%s" ("key", "data", "tags", "expire", "dated") 42 | VALUES (:key, :data, :tags, :exp, :dated);', 43 | 'delete' => 'DELETE FROM "%s" WHERE "key"=?;', 44 | 'clean' => 'DELETE FROM "%s" WHERE %s;', // %s 'clean_like' iterated 45 | 'clean_like'=> 'tags LIKE ?', 46 | 'flush_all' => 'DELETE FROM "%s";', 47 | 'flush' => 'DELETE FROM "%s" WHERE "key" LIKE ? OR "tags" LIKE ?;', 48 | 'purge' => 'DELETE FROM "%s" WHERE "expire" IS NOT NULL AND "expire" < %d;' 49 | ); 50 | 51 | /** 52 | * Constructor. 53 | * 54 | * @param \PDO $pdo 55 | * @param array $options Array of options. 56 | */ 57 | public function __construct(\PDO $pdo, array $options=null) 58 | { 59 | parent::__construct($pdo, $options); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Pdo/Sql1999.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Pdo; 14 | 15 | use Apix\Cache\AbstractPdo; 16 | 17 | /** 18 | * The SQL:1999 / SQL 3 (PDO) cache wrapper. 19 | * 20 | * Allows to use databases that have 'some' SQL:1999 compliance. 21 | * e.g. Oracle, IBM DB2, Informix. 22 | * PostgreSQL, MySQl and SQLite should also support SQL99. 23 | * 24 | * Conforms to at least SQL-92 are DB2, MSSQL, MySQL, Oracle, Informix. 25 | * 26 | * @author Franck Cassedanne 27 | */ 28 | class Sql1999 extends AbstractPdo 29 | { 30 | 31 | /** 32 | * Holds a generic SQL-99'ish schema definitions. 33 | * @see http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt 34 | */ 35 | protected $sql_definitions = array( 36 | 'init' => 'CREATE TABLE "%s" ("key" VARCHAR PRIMARY KEY, "data" TEXT, 37 | "tags" TEXT, "expire" INTEGER, "dated" TIMESTAMP);', 38 | 'key_idx' => 'CREATE INDEX "%s_key_idx" ON "%s" ("key");', 39 | 'exp_idx' => 'CREATE INDEX "%s_exp_idx" ON "%s" ("expire");', 40 | 'tag_idx' => 'CREATE INDEX "%s_tag_idx" ON "%s" ("tags");', 41 | 'loadKey' => 'SELECT "data", "expire" FROM "%s" WHERE "key"=:key AND 42 | ("expire" IS NULL OR "expire" > :now);', 43 | 'loadTag' => 'SELECT "key" FROM "%s" WHERE "tags" LIKE :tag AND 44 | ("expire" IS NULL OR "expire" > :now);', 45 | 'update' => 'UPDATE "%s" SET "data"=:data, "tags"=:tags, "expire"=:exp, 46 | "dated"=:dated WHERE "key"=:key;', 47 | 'insert' => 'INSERT INTO "%s" ("key", "data", "tags", "expire", "dated") 48 | VALUES (:key, :data, :tags, :exp, :dated);', 49 | 'delete' => 'DELETE FROM "%s" WHERE "key"=?;', 50 | 'clean' => 'DELETE FROM "%s" WHERE %s;', // %s 'clean_like' iterated 51 | 'clean_like'=> 'tags LIKE ?', 52 | 'flush_all' => 'DELETE FROM "%s";', 53 | 'flush' => 'DELETE FROM %s WHERE key LIKE ? OR tags LIKE ?;', 54 | 'purge' => 'DELETE FROM "%s" WHERE "expire" IS NOT NULL AND "expire" < %d;' 55 | ); 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Pdo/Sqlite.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Pdo; 14 | 15 | use Apix\Cache\AbstractPdo; 16 | 17 | /** 18 | * The SQLite (PDO) wrapper. 19 | * 20 | * @author Franck Cassedanne 21 | */ 22 | class Sqlite extends AbstractPdo 23 | { 24 | 25 | /** 26 | * Holds the SQL definitions for SQLite v2/v3. 27 | */ 28 | protected $sql_definitions = array( 29 | 'init' => 'CREATE TABLE IF NOT EXISTS %s (key VARCHAR PRIMARY KEY, 30 | data TEXT, tags TEXT, expire INTEGER, dated TIMESTAMP);', 31 | 'key_idx' => 'CREATE INDEX IF NOT EXISTS %s_key_idx ON %s (key);', 32 | 'exp_idx' => 'CREATE INDEX IF NOT EXISTS %s_exp_idx ON %s (expire);', 33 | 'tag_idx' => 'CREATE INDEX IF NOT EXISTS %s_tag_idx ON %s (tags);', 34 | 'loadKey' => 'SELECT data, expire FROM %s WHERE key=:key AND 35 | (expire IS NULL OR expire > :now);', 36 | 'loadTag' => 'SELECT key FROM %s WHERE tags LIKE :tag AND 37 | (expire IS NULL OR expire > :now);', 38 | 'update' => 'UPDATE %s SET data=:data, tags=:tags, expire=:exp, 39 | dated=:dated WHERE key=:key;', 40 | 'insert' => 'INSERT INTO %s (key, data, tags, expire, dated) 41 | VALUES (:key, :data, :tags, :exp, :dated);', 42 | 'delete' => 'DELETE FROM %s WHERE key = ?;', 43 | 'clean' => 'DELETE FROM %s WHERE %s;', // %s 'clean_like' iterated 44 | 'clean_like'=> 'tags LIKE ?', 45 | 'flush_all' => 'DELETE FROM %s;', 46 | 'flush' => 'DELETE FROM %s WHERE key LIKE ? OR tags LIKE ?;', 47 | 'purge' => 'DELETE FROM %s WHERE expire IS NOT NULL AND expire < %d;' 48 | ); 49 | 50 | /** 51 | * Constructor. 52 | * 53 | * @param \PDO $pdo 54 | * @param array $options Array of options. 55 | */ 56 | public function __construct(\PDO $pdo, array $options=null) 57 | { 58 | // Set some SQLite PRAGMA to speed things up. 59 | // @see http://www.sqlite.org/pragma.html 60 | // @see http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite 61 | $pdo->exec('PRAGMA synchronous=OFF'); 62 | $pdo->exec('PRAGMA journal_mode=MEMORY'); 63 | $pdo->exec('PRAGMA temp_store=MEMORY'); 64 | $pdo->exec('PRAGMA count_changes=false'); 65 | 66 | parent::__construct($pdo, $options); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/PsrCache/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\PsrCache; 14 | 15 | /** 16 | * Exception for invalid cache arguements. 17 | */ 18 | class InvalidArgumentException 19 | extends \InvalidArgumentException 20 | implements \Psr\Cache\InvalidArgumentException 21 | { } 22 | -------------------------------------------------------------------------------- /src/PsrCache/Item.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\PsrCache; 14 | 15 | use Psr\Cache\CacheItemInterface as ItemInterface; 16 | use Psr\Cache\CacheItemPoolInterface as ItemPoolInterface; 17 | 18 | class Item implements ItemInterface 19 | { 20 | const DEFAULT_EXPIRATION = 'now +1 year'; 21 | 22 | /** 23 | * The cache key for the item. 24 | * @var string 25 | */ 26 | protected $key; 27 | 28 | /** 29 | * The raw (unserialized) cached value. 30 | * @var mixed 31 | */ 32 | protected $value; 33 | 34 | /** 35 | * Wether the item has been saved to the cache yet. 36 | * @var bool 37 | */ 38 | protected $hit = false; 39 | 40 | /** 41 | * The expiration date. 42 | * @var \DateTime 43 | */ 44 | protected $expiration; 45 | 46 | /** 47 | * Constructs a new Item. 48 | * You should never use this directly. It is used internally to create items 49 | * from the pool. 50 | * @param string $key The item key 51 | * @param mixed $value The item value (unserialized) 52 | * @param \DateTime|integer|null $ttl 53 | * @param bool $hit Was this item retrived from cache? 54 | */ 55 | public function __construct($key, $value = null, $ttl = null, $hit = false) 56 | { 57 | $this->key = self::normalizedKey($key); 58 | $this->value = $value; 59 | $this->hit = $hit; 60 | $this->expiresAt($ttl); 61 | } 62 | 63 | /** 64 | * Returns a normalised key. 65 | * 66 | * @return string 67 | */ 68 | public static function normalizedKey($key) 69 | { 70 | if (!is_string($key) || empty($key) || strpbrk($key, '{}()/\@:') ) { 71 | throw new InvalidArgumentException( 72 | 'Item key (' . var_export($key, true) . ') is invalid.' 73 | ); 74 | } 75 | 76 | return $key; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function getKey() 83 | { 84 | return $this->key; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function get() 91 | { 92 | return $this->hit ? $this->value : null; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function set($value = null) 99 | { 100 | $this->value = $value; 101 | $this->hit = false; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function isHit() 110 | { 111 | return $this->hit && $this->getTtlInSecond() > 0; 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function getExpiration() 118 | { 119 | return $this->expiration; 120 | } 121 | 122 | /** 123 | * Returns the time to live in second. 124 | * 125 | * @return integer 126 | */ 127 | public function getTtlInSecond() 128 | { 129 | return $this->expiration->format('U') - time(); 130 | } 131 | 132 | /** 133 | * Sets the cache hit for this item. 134 | * 135 | * @param boolean $hit 136 | * @return static The invoked object. 137 | */ 138 | public function setHit($hit) 139 | { 140 | $this->hit = (bool) $hit; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function expiresAt($expiration = null) 149 | { 150 | if ($expiration instanceof \DateTime) { 151 | $this->expiration = $expiration; 152 | 153 | } elseif (is_int($expiration)) { 154 | $this->expiration = new \DateTime( 155 | 'now +' . $expiration . ' seconds' 156 | ); 157 | 158 | } elseif (null === $expiration) { 159 | $this->expiration = new \DateTime(self::DEFAULT_EXPIRATION); 160 | 161 | } else { 162 | 163 | throw new InvalidArgumentException( 164 | 'Integer or \DateTime object expected.' 165 | ); 166 | } 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function expiresAfter($time) 175 | { 176 | if ($time instanceof \DateInterval) { 177 | $this->expiration = new \DateTime(); 178 | $this->expiration->add($time); 179 | 180 | } elseif (is_int($time)) { 181 | $this->expiration = new \DateTime('now +' . $time . ' seconds'); 182 | 183 | } elseif (null === $time) { 184 | $this->expiration = new \DateTime(self::DEFAULT_EXPIRATION); 185 | 186 | } else { 187 | 188 | throw new InvalidArgumentException( 189 | 'Integer or \DateInterval object expected.' 190 | ); 191 | } 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Returns the item value. 198 | * 199 | * @return string 200 | */ 201 | public function __toString() 202 | { 203 | return $this->value; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/PsrCache/Pool.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\PsrCache; 14 | 15 | use Apix\Cache\Adapter as CacheAdapter; 16 | use Psr\Cache\CacheItemInterface as ItemInterface; 17 | use Psr\Cache\CacheItemPoolInterface as ItemPoolInterface; 18 | 19 | class Pool implements ItemPoolInterface 20 | { 21 | 22 | /** 23 | * 24 | * @var CacheAdapter 25 | */ 26 | protected $cache_adapter; 27 | 28 | /** 29 | * Deferred cache items to be saved later. 30 | * 31 | * @var array Collection of \Apix\PsrCache\Item. 32 | */ 33 | protected $deferred = array(); 34 | 35 | /** 36 | * Constructor. 37 | */ 38 | public function __construct(CacheAdapter $cache_adapter) 39 | { 40 | $this->cache_adapter = $cache_adapter; 41 | 42 | $options = array( 43 | 'tag_enable' => false // wether to enable tagging 44 | ); 45 | $this->cache_adapter->setOptions($options); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getItem($key) 52 | { 53 | $key = Item::normalizedKey($key); 54 | 55 | if (isset($this->deferred[$key])) { 56 | return $this->deferred[$key]; 57 | } 58 | 59 | $value = $this->cache_adapter->loadKey($key); 60 | 61 | return new Item( 62 | $this->cache_adapter->removePrefixKey($key), 63 | $value, 64 | $this->cache_adapter->getTtl($key) ?: null, 65 | (bool) $value // indicates wether it is loaded from cache or not. 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getItems(array $keys = array()) 73 | { 74 | $items = array(); 75 | foreach ($keys as $key) { 76 | $items[$key] = $this->getItem($key); 77 | } 78 | 79 | return $items; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function hasItem($key) 86 | { 87 | return $this->getItem($key)->isHit(); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function clear() 94 | { 95 | $this->deferred = array(); 96 | return $this->cache_adapter->flush(true); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function deleteItems(array $keys) 103 | { 104 | $checks = array(); 105 | foreach ($keys as $key) { 106 | // Only delete from cache if it actually exists 107 | if($this->getItem($key)->isHit()) { 108 | $checks[] = $this->cache_adapter->delete($key); 109 | } 110 | unset($this->deferred[$key]); 111 | } 112 | return (bool) !in_array(false, $checks, true); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function deleteItem($key) 119 | { 120 | return $this->deleteItems(array($key)); 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function save(ItemInterface $item) 127 | { 128 | $ttl = $item->getTtlInSecond(); 129 | 130 | $item->setHit(true); 131 | $success = $this->cache_adapter->save( 132 | $item->get(), // value to store 133 | $item->getKey(), // its key 134 | null, // disable tags support 135 | is_null($ttl) ? 0 : $ttl // ttl in sec or null forever 136 | ); 137 | $item->setHit($success); 138 | 139 | return $success; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function saveDeferred(ItemInterface $item) 146 | { 147 | $this->deferred[$item->getKey()] = $item; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function commit() 156 | { 157 | foreach ($this->deferred as $key => $item) { 158 | $this->save($item); 159 | if ( $item->isHit() ) { 160 | unset($this->deferred[$key]); 161 | } 162 | } 163 | 164 | return empty($this->deferred); 165 | } 166 | 167 | /** 168 | * Returns the cache adapter for this pool. 169 | * 170 | * @return CacheAdapter 171 | */ 172 | public function getCacheAdapter() 173 | { 174 | return $this->cache_adapter; 175 | } 176 | 177 | /** 178 | * Commit the deferred items ~ acts as the last resort garbage collector. 179 | */ 180 | public function __destruct() 181 | { 182 | $this->commit(); 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/PsrCache/TaggableItem.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\PsrCache; 14 | 15 | class TaggableItem extends Item 16 | { 17 | 18 | /** 19 | * The tags associated with this entry. 20 | * @var array|null 21 | */ 22 | protected $tags = null; 23 | 24 | /** 25 | * Sets this item tags. 26 | * 27 | * @param array|null $tags 28 | * @return TaggableItem The invoked object. 29 | */ 30 | public function setTags(array $tags=null) 31 | { 32 | $this->tags = $tags; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Returns this item tags. 39 | * 40 | * @return array|null 41 | */ 42 | public function getTags() 43 | { 44 | return $this->tags; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/PsrCache/TaggablePool.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\PsrCache; 14 | 15 | use Apix\Cache\Adapter as CacheAdapter; 16 | use Psr\Cache\CacheItemInterface as ItemInterface; 17 | 18 | class TaggablePool extends Pool 19 | { 20 | 21 | /** 22 | * The tags associated with this pool (just an optimisation hack). 23 | * @var array|null 24 | */ 25 | private $_tags = null; 26 | 27 | /** 28 | * Constructor. 29 | */ 30 | public function __construct(CacheAdapter $cache_adapter) 31 | { 32 | $this->cache_adapter = $cache_adapter; 33 | 34 | $options = array( 35 | 'tag_enable' => true // wether to enable tagging 36 | ); 37 | $this->cache_adapter->setOptions($options); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getItem($key) 44 | { 45 | $value = $this->cache_adapter->loadKey($key); 46 | 47 | $item = new TaggableItem( 48 | $this->cache_adapter->removePrefixKey($key), 49 | $value, 50 | $this->cache_adapter->getTtl($key) ?: null, 51 | (bool) $value // indicates wether it is loaded from cache or not. 52 | ); 53 | 54 | // Set this pool tags rather than the actual cached item tags. 55 | $item->setTags($this->_tags); 56 | 57 | return $item; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function save(ItemInterface $item) 64 | { 65 | $ttl = $item->getTtlInSecond(); 66 | $this->_tags = $item->getTags(); 67 | 68 | $item->setHit(true); 69 | $success = $this->cache_adapter->save( 70 | $item->get(), // value to store 71 | $item->getKey(), // its key 72 | $this->_tags, // this pool tags 73 | is_null($ttl) ? 0 : $ttl // ttl in sec or null for ever 74 | ); 75 | $item->setHit($success); 76 | 77 | return $success; 78 | } 79 | 80 | /** 81 | * Retrieves the cache keys for the given tag. 82 | * 83 | * @param string $tag The cache tag to retrieve. 84 | * @return array Returns an array of cache keys. 85 | */ 86 | public function getItemsByTag($tag) 87 | { 88 | $keys = $this->cache_adapter->loadTag($tag); 89 | $items = array(); 90 | if ($keys) { 91 | foreach ($keys as $key) { 92 | $k = $this->cache_adapter->removePrefixKey($key); 93 | $items[$k] = $this->getItem($k); 94 | } 95 | } 96 | 97 | return $items; 98 | } 99 | 100 | /** 101 | * Removes all the cached entries associated with the given tag names. 102 | * 103 | * @param array $tags The array of tags to remove. 104 | * @return bool 105 | */ 106 | public function clearByTags(array $tags) 107 | { 108 | return $this->cache_adapter->clean($tags); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/Redis.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache; 14 | 15 | /** 16 | * Redis/PhpRedis cache wrapper. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | class Redis extends AbstractCache 21 | { 22 | 23 | /** 24 | * Constructor. 25 | * 26 | * @param \Redis $redis A Redis client instance. 27 | * @param array $options Array of options. 28 | */ 29 | public function __construct(\Redis $redis, array $options=null) 30 | { 31 | $options['atomicity'] = !isset($options['atomicity']) 32 | || true === $options['atomicity'] 33 | ? \Redis::MULTI 34 | : \Redis::PIPELINE; 35 | 36 | $this->options['serializer'] = 'php'; // null, php, igBinary, json, 37 | // msgpack 38 | 39 | parent::__construct($redis, $options); 40 | 41 | $this->setSerializer($this->options['serializer']); 42 | $redis->setOption( \Redis::OPT_SERIALIZER, $this->getSerializer() ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function loadKey($key) 49 | { 50 | $cache = $this->adapter->get($this->mapKey($key)); 51 | 52 | return false === $cache ? null : $cache; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function loadTag($tag) 59 | { 60 | $cache = $this->adapter->sMembers($this->mapTag($tag)); 61 | 62 | return empty($cache) ? null : $cache; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function save($data, $key, array $tags=null, $ttl=null) 69 | { 70 | $key = $this->mapKey($key); 71 | 72 | if (null === $ttl || 0 === $ttl) { 73 | $success = $this->adapter->set($key, $data); 74 | } else { 75 | $success = $this->adapter->setex($key, $ttl, $data); 76 | } 77 | 78 | if ($success && $this->options['tag_enable'] && !empty($tags)) { 79 | $redis = $this->adapter->multi($this->options['atomicity']); 80 | foreach ($tags as $tag) { 81 | $redis->sAdd($this->mapTag($tag), $key); 82 | } 83 | $redis->exec(); 84 | } 85 | 86 | return $success; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function clean(array $tags) 93 | { 94 | $items = array(); 95 | foreach ($tags as $tag) { 96 | $keys = $this->loadTag($tag); 97 | if (is_array($keys)) { 98 | array_walk_recursive( 99 | $keys, 100 | function ($key) use (&$items) { $items[] = $key; } 101 | ); 102 | } 103 | $items[] = $this->mapTag($tag); 104 | } 105 | 106 | return (boolean) $this->adapter->del($items); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function delete($key) 113 | { 114 | $key = $this->mapKey($key); 115 | 116 | if ($this->options['tag_enable']) { 117 | $tags = $this->adapter->keys($this->mapTag('*')); 118 | if (!empty($tags)) { 119 | $redis = $this->adapter->multi($this->options['atomicity']); 120 | foreach ($tags as $tag) { 121 | $redis->sRem($tag, $key); 122 | } 123 | $redis->exec(); 124 | } 125 | } 126 | 127 | return (boolean) $this->adapter->del($key); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function flush($all=false) 134 | { 135 | if (true === $all) { 136 | return (boolean) $this->adapter->flushDb(); 137 | } 138 | $items = array_merge( 139 | $this->adapter->keys($this->mapTag('*')), 140 | $this->adapter->keys($this->mapKey('*')) 141 | ); 142 | 143 | return (boolean) $this->adapter->del($items); 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | * @param string $serializer 149 | */ 150 | public function setSerializer($serializer) 151 | { 152 | switch ($serializer) { 153 | case 'json': 154 | // $this->serializer = \Redis::SERIALIZER_JSON; 155 | parent::setSerializer($serializer); 156 | break; 157 | 158 | case 'php': 159 | $this->serializer = \Redis::SERIALIZER_PHP; 160 | break; 161 | 162 | // @codeCoverageIgnoreStart 163 | case 'igBinary': 164 | // igBinary is not always compiled on the host machine. 165 | $this->serializer = \Redis::SERIALIZER_IGBINARY; 166 | break; 167 | 168 | case 'msgpack': 169 | // available on PHP7 since msgpack 2.0.1 170 | $this->serializer = \Redis::SERIALIZER_MSGPACK; 171 | break; 172 | // @codeCoverageIgnoreEnd 173 | 174 | default: 175 | $this->serializer = \Redis::SERIALIZER_NONE; 176 | } 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function getTtl($key) 183 | { 184 | $ttl = $this->adapter->ttl($this->mapKey($key)); 185 | 186 | if ($ttl == -2) { 187 | return false; 188 | } 189 | 190 | return $ttl > -1 ? $ttl : 0; 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/Runtime.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 9 | * 10 | */ 11 | 12 | namespace Apix\Cache; 13 | 14 | /** 15 | * Runtime (Array/ArrayObject) cache wrapper. 16 | * 17 | * @author Franck Cassedanne 18 | */ 19 | class Runtime extends AbstractCache 20 | { 21 | /** 22 | * Holds the cached items. 23 | * e.g. ['key' => ['data', 'tags', 'expire']] 24 | * @var array|\ArrayObject|\Traversable 25 | */ 26 | protected $items; 27 | 28 | /** 29 | * Constructor. 30 | * 31 | * @param array|\Traversable|null $mix 32 | * @param array|null $options An array of user options. 33 | */ 34 | public function __construct($mix = null, array $options=null) 35 | { 36 | if(null === $mix) { 37 | $this->items = new \ArrayObject(); 38 | } else if(is_array($mix) || $mix instanceof \Traversable) { 39 | $this->items = $mix; 40 | } else { 41 | throw new \Apix\Cache\Exception("Error Processing Request"); 42 | } 43 | 44 | parent::__construct(null, $options); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function loadKey($key) 51 | { 52 | $key = $this->mapKey($key); 53 | 54 | return isset($this->items[$key]) ? $this->items[$key]['data'] : null; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function loadTag($tag) 61 | { 62 | if ($this->options['tag_enable']) { 63 | $tag = $this->mapTag($tag); 64 | $keys = array(); 65 | foreach ($this->items as $key => $data) { 66 | if( isset($data['tags']) 67 | && false !== array_search($tag, $data['tags']) 68 | ) { 69 | $keys[] = $key; 70 | } 71 | } 72 | 73 | return empty($keys) ? null : $keys; 74 | } 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function save($data, $key, array $tags=null, $ttl=null) 81 | { 82 | $key = $this->mapKey($key); 83 | $this->items[$key] = array( 84 | 'data' => $data, 85 | 'expire' => $ttl 86 | ); 87 | 88 | if ($this->options['tag_enable'] && !empty($tags)) { 89 | $_tags = array(); 90 | foreach ($tags as $tag) { 91 | $_tags[] = $this->mapTag($tag); 92 | } 93 | $this->items[$key]['tags'] = array_unique($_tags); 94 | } 95 | 96 | return true; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function delete($key) 103 | { 104 | $key = $this->mapKey($key); 105 | 106 | if ( isset($this->items[$key]) ) { 107 | unset($this->items[$key]); 108 | 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function clean(array $tags) 119 | { 120 | $rmed = array(); 121 | foreach ($tags as $tag) { 122 | $keys = $this->loadTag($tag); 123 | if ($keys) { 124 | foreach ($keys as $key) { 125 | unset($this->items[$key]); 126 | } 127 | } else { 128 | $rmed[] = false; 129 | } 130 | } 131 | 132 | return !in_array(false, $rmed); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function flush($all=false) 139 | { 140 | $this->items = new \ArrayObject(); 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function getTtl($key) 149 | { 150 | $key = $this->mapKey($key); 151 | 152 | return isset($this->items[$key]) ? $this->items[$key]['expire'] : false; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/Serializer/Adapter.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * The interface adapter for the cache serializer. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | interface Adapter 21 | { 22 | 23 | /** 24 | * Serialises mixed data as a string. 25 | * 26 | * @param mixed $data 27 | * @return string|mixed 28 | */ 29 | public function serialize($data); 30 | 31 | /** 32 | * Unserialises a string representation as mixed data. 33 | * 34 | * @param string $str 35 | * @return mixed|string 36 | */ 37 | public function unserialize($str); 38 | 39 | /** 40 | * Checks if the input is a serialized string representation. 41 | * 42 | * @param string $str 43 | * @return boolean 44 | * @deprecated 45 | */ 46 | public function isSerialized($str); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Serializer/Igbinary.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * IgBinary - fast binary serializer. 17 | * Serializes cache data using the IgBinary extension. 18 | * @see https://github.com/IgBinary/IgBinary 19 | * 20 | * @author Franck Cassedanne 21 | */ 22 | class Igbinary implements Adapter 23 | { 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function serialize($data) 29 | { 30 | return \igbinary_serialize($data); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function unserialize($str) 37 | { 38 | return \igbinary_unserialize($str); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function isSerialized($str) 45 | { 46 | if (!is_string($str)) { 47 | return false; 48 | } 49 | 50 | return @substr_count($str, "\000", 0, 3) == 3; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Serializer/Json.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * Serializes data using the native PHP Json extension. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | class Json implements Adapter 21 | { 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function serialize($data) 27 | { 28 | return json_encode($data); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function unserialize($str) 35 | { 36 | return json_decode($str); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isSerialized($str) 43 | { 44 | if (!is_string($str)) { 45 | return false; 46 | } 47 | 48 | return (boolean) (json_decode($str) !== null); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Serializer/Msgpack.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * MessagePack - a light cross-language binary serializer. 17 | * Serializes data using the Msgpack extension. 18 | * @see https://github.com/msgpack/msgpack-php 19 | * 20 | * @author Franck Cassedanne 21 | */ 22 | class Msgpack implements Adapter 23 | { 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function serialize($data) 29 | { 30 | return \msgpack_pack($data); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function unserialize($str) 37 | { 38 | return \msgpack_unpack($str); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function isSerialized($str) 45 | { 46 | if (!is_string($str)) { 47 | return false; 48 | } 49 | 50 | return (boolean) !is_integer( @\msgpack_unpack($str) ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Serializer/None.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * Blank/null/none serializer. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | class None implements Adapter 21 | { 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function serialize($data) 27 | { 28 | return $data; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function unserialize($str) 35 | { 36 | return $str; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isSerialized($str) 43 | { 44 | return false; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Serializer/Php.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * Serializes data using the native PHP serializer. 17 | * 18 | * @author Franck Cassedanne 19 | */ 20 | class Php implements Adapter 21 | { 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function serialize($data) 27 | { 28 | return serialize($data); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function unserialize($str) 35 | { 36 | return unserialize($str); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isSerialized($str) 43 | { 44 | if (!is_string($str)) { 45 | return false; 46 | } 47 | 48 | return (boolean) ($str=='b:0;' || @unserialize($str) !== false); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Serializer/Stringset.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\Serializer; 14 | 15 | /** 16 | * Serializes cache data using a stringset (appendset) such as: 17 | * 'tag1 tag2 -tag2 -tag3' 18 | * 19 | * @author Franck Cassedanne 20 | */ 21 | class Stringset implements Adapter 22 | { 23 | 24 | /** 25 | * Holds this string dirtiness. 26 | * @var integer 27 | */ 28 | protected $dirtiness; 29 | 30 | /** 31 | * {@inheritdoc} 32 | * 33 | * e.g. ['a','c'] => 'a b ' 34 | */ 35 | public function serialize($keys, $op='') 36 | { 37 | $str = ''; 38 | foreach ((array) $keys as $key) { 39 | $str .= "${op}${key} "; 40 | } 41 | 42 | return $str != '' ? $str : null; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | * 48 | * e.g. 'a b c -b -x ' => ['a','c']; 49 | * Sets the dirtiness level (counts the negative entries). 50 | */ 51 | public function unserialize($str) 52 | { 53 | $add = array(); 54 | $remove = array(); 55 | foreach (explode(' ', trim($str)) as $key) { 56 | if (isset($key[0])) { 57 | if ($key[0] == '-') { 58 | $remove[] = substr($key, 1); 59 | } else { 60 | $add[] = $key; 61 | } 62 | } 63 | } 64 | 65 | $this->dirtiness = count($remove); 66 | 67 | $items = array_values(array_diff($add, $remove)); 68 | 69 | return empty($items) ? null : $items; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function isSerialized($str) 76 | { 77 | if (!is_string($str)) { 78 | return false; 79 | } 80 | 81 | return (boolean) preg_match('/^.*\s$/', $str); 82 | } 83 | 84 | /** 85 | * Returns the dirtness level of the userialized string. 86 | * 87 | * @return integer 88 | */ 89 | public function getDirtiness() 90 | { 91 | return $this->dirtiness; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /tests/AbstractCacheTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | class AbstractCacheTest extends TestCase 18 | { 19 | protected $cache = null; 20 | 21 | public function setUp() 22 | { 23 | $this->cache = new Cache\Runtime(null, $this->options); 24 | } 25 | 26 | public function tearDown() 27 | { 28 | if (null !== $this->cache) { 29 | $this->cache->flush(); 30 | unset($this->cache); 31 | } 32 | } 33 | 34 | public function testGetOption() 35 | { 36 | $this->assertSame( 37 | $this->options['prefix_key'], 38 | $this->cache->getOption('prefix_key') 39 | ); 40 | } 41 | 42 | /** 43 | * @expectedException Apix\Cache\PsrCache\InvalidArgumentException 44 | */ 45 | public function testGetOptionThrowAnInvalidArgumentException() 46 | { 47 | $this->cache->getOption('key'); 48 | } 49 | 50 | public function testSetOption() 51 | { 52 | $this->cache->setOption('prefix_key', 'foo'); 53 | 54 | $this->assertSame('foo', $this->cache->getOption('prefix_key')); 55 | } 56 | 57 | public function testGetAdapter() 58 | { 59 | $this->assertNull( $this->cache->getAdapter() ); 60 | } 61 | 62 | public function testGetSetSerializer() 63 | { 64 | $this->cache->setSerializer(null); 65 | $this->assertNull( $this->cache->getSerializer() ); 66 | 67 | $this->cache->setSerializer('none'); 68 | $this->assertInstanceOf( 69 | '\Apix\Cache\Serializer\None', 70 | $this->cache->getSerializer() 71 | ); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /tests/ApcTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | /** 18 | * ApcTest, supports both APC and APCu user cache. 19 | * 20 | * usage: php -d apc.enable_cli=1 `which phpunit` -v tests/ApcTest.php 21 | * 22 | * @package Apix\Cache 23 | * @author Franck Cassedanne 24 | */ 25 | class ApcTest extends GenericTestCase 26 | { 27 | protected $cache = null; 28 | 29 | public function setUp() 30 | { 31 | $this->skipIfMissing('apc'); 32 | 33 | if (!ini_get('apc.enable_cli')) { 34 | self::markTestSkipped( 35 | 'apc.enable_cli MUST be enabled in order to run this unit test' 36 | ); 37 | } 38 | 39 | $this->cache = new Cache\Apc($this->options); 40 | } 41 | 42 | public function tearDown() 43 | { 44 | if (null !== $this->cache) { 45 | $this->cache->flush(); 46 | unset($this->cache); 47 | } 48 | } 49 | 50 | public function testComplyWithApc() 51 | { 52 | $this->assertTrue($this->cache->save('data', 'id')); 53 | $id = $this->cache->mapKey('id'); 54 | $this->assertEquals('data', apc_fetch($id)); 55 | } 56 | 57 | public function testFlushSelected() 58 | { 59 | $this->assertTrue( 60 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 61 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 62 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 63 | ); 64 | apc_add('foo', 'bar'); 65 | $this->assertTrue($this->cache->flush()); 66 | $this->assertFalse($this->cache->flush()); 67 | $this->assertEquals('bar', apc_fetch('foo')); 68 | 69 | $this->assertNull($this->cache->load('id3')); 70 | $this->assertNull($this->cache->load('tag1', 'tag')); 71 | } 72 | 73 | public function testFlushAll() 74 | { 75 | $this->assertTrue( 76 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 77 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 78 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 79 | ); 80 | 81 | apc_add('foo', 'bar'); 82 | $this->assertTrue($this->cache->flush(true)); // always true! 83 | 84 | $this->assertEquals(false, apc_fetch('foo')); 85 | 86 | $this->assertNull($this->cache->load('id3')); 87 | $this->assertNull($this->cache->load('tag1', 'tag')); 88 | } 89 | 90 | /** 91 | * -runTestsInSeparateProcesses 92 | * -runInSeparateProcesses 93 | * -preserveGlobalState enable 94 | * -depends test 95 | */ 96 | public function testShortTtlDoesExpunge() 97 | { 98 | $this->markTestSkipped( 99 | "APC will only expunged its cache on the next request which makes " 100 | . "this specific unit untestable!?... :-(" 101 | ); 102 | $this->cache->save('ttl-1', 'ttlId', null, -1); 103 | // $this->assertSame('ttl-1', apc_fetch($this->cache->mapKey('ttlId'))); 104 | // $this->assertSame('ttl-1', $this->cache->load('ttlId')); 105 | $this->assertNull( $this->cache->load('ttlId'), "Should be null"); 106 | } 107 | 108 | public function testGetInternalInfos() 109 | { 110 | $this->cache->save('someData', 'someId', null, 69); 111 | $infos = $this->cache->getInternalInfos('someId'); 112 | $this->assertSame(69, $infos['ttl']); 113 | } 114 | 115 | public function testGetInternalInfosReturnFalseWhenNonExistant() 116 | { 117 | $this->assertFalse( 118 | $this->cache->getInternalInfos('non-existant') 119 | ); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /tests/ApcuTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | /** 18 | * ApcTest, supports both APC and APCu user cache. 19 | * 20 | * usage: php -d apc.enable_cli=1 `which phpunit` -v tests/ApcTest.php 21 | * 22 | * @package Apix\Cache 23 | * @author Franck Cassedanne 24 | */ 25 | class ApcuTest extends ApcTest 26 | { 27 | public function setUp() 28 | { 29 | $this->skipIfMissing('apcu'); 30 | 31 | if (!ini_get('apc.enable_cli')) { 32 | self::markTestSkipped( 33 | 'apc.enable_cli MUST be enabled in order to run this unit test' 34 | ); 35 | } 36 | 37 | $this->cache = new Cache\Apcu($this->options); 38 | } 39 | 40 | public function testComplyWithApc() 41 | { 42 | $this->assertTrue($this->cache->save('data', 'id')); 43 | $id = $this->cache->mapKey('id'); 44 | $this->assertEquals('data', apcu_fetch($id)); 45 | } 46 | 47 | public function testFlushSelected() 48 | { 49 | $this->assertTrue( 50 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 51 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 52 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 53 | ); 54 | apcu_add('foo', 'bar'); 55 | $this->assertTrue($this->cache->flush()); 56 | $this->assertFalse($this->cache->flush()); 57 | $this->assertEquals('bar', apcu_fetch('foo')); 58 | 59 | $this->assertNull($this->cache->load('id3')); 60 | $this->assertNull($this->cache->load('tag1', 'tag')); 61 | } 62 | 63 | public function testFlushAll() 64 | { 65 | $this->assertTrue( 66 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 67 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 68 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 69 | ); 70 | 71 | apcu_add('foo', 'bar'); 72 | $this->assertTrue($this->cache->flush(true)); // always true! 73 | 74 | $this->assertEquals(false, apcu_fetch('foo')); 75 | 76 | $this->assertNull($this->cache->load('id3')); 77 | $this->assertNull($this->cache->load('tag1', 'tag')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | class DirectoryTest extends GenericTestCase 18 | { 19 | /** @var null|Cache\Directory */ 20 | protected $cache = null; 21 | 22 | /** @var string **/ 23 | protected $dir = null; 24 | 25 | public function setUp() 26 | { 27 | $this->dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'apix-cache-unittest'; 28 | 29 | $this->cache = new Cache\Directory( 30 | $this->options + array('directory' => $this->dir) 31 | ); 32 | } 33 | 34 | public function tearDown() 35 | { 36 | if (null !== $this->cache) { 37 | $this->cache->flush(); 38 | 39 | $this->cache->delTree( $this->dir ); 40 | unset($this->cache); 41 | } 42 | } 43 | 44 | public function testNonExistsExpire() 45 | { 46 | $this->assertTrue($this->cache->save('data', 'id', null, 1)); 47 | $this->assertTrue($this->cache->save('data', 'id')); 48 | 49 | $this->assertEquals(0, $this->cache->getTtl('id')); 50 | } 51 | 52 | public function testDeleteWithNoTag() 53 | { 54 | $this->cache->setOption('tag_enable', false); 55 | $this->assertTrue($this->cache->save('data', 'id', array('tag'))); 56 | $this->assertTrue($this->cache->delete('id')); 57 | } 58 | 59 | /** 60 | * Regression test for pull request GH#17 61 | * 62 | * @link https://github.com/frqnck/apix-cache/pull/17/files 63 | * "File and Directory Adapters @clean returns when fails to find a 64 | * key within a tag" 65 | * @see FilesTest\testPullRequest17() 66 | * @group pr 67 | */ 68 | public function testPullRequest17() 69 | { 70 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')); 71 | 72 | $this->assertNotNull($this->cache->loadTag('tag2')); 73 | $this->assertTrue($this->cache->clean(array('non-existant', 'tag2'))); 74 | $this->assertNull($this->cache->loadTag('tag2')); 75 | 76 | // for good measure. 77 | $this->assertFalse($this->cache->clean(array('tag2'))); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /tests/FactoryTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | class FactoryTest extends TestCase 18 | { 19 | protected $cache = null; 20 | 21 | public function setUp() 22 | { 23 | } 24 | 25 | public function tearDown() 26 | { 27 | if (null !== $this->cache) { 28 | $this->cache->flush(); 29 | unset($this->cache); 30 | } 31 | } 32 | 33 | /** 34 | * @expectedException Apix\Cache\PsrCache\InvalidArgumentException 35 | */ 36 | public function testPoolWithUnsurportedObjectThrowsException() 37 | { 38 | Cache\Factory::getPool( new \StdClass() ); 39 | } 40 | 41 | public function testPoolFromCacheClientObject() 42 | { 43 | $adapter = new \ArrayObject(); 44 | $pool = Cache\Factory::getPool($adapter, $this->options); 45 | $this->assertInstanceOf('\Apix\Cache\PsrCache\Pool', $pool); 46 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 47 | } 48 | 49 | /** 50 | * @expectedException Apix\Cache\PsrCache\InvalidArgumentException 51 | */ 52 | public function testPoolWithUnsurportedStringThrowsException() 53 | { 54 | Cache\Factory::getPool('non-existant', $this->options); 55 | } 56 | 57 | public function testPoolFromString() 58 | { 59 | $pool = Cache\Factory::getPool('Runtime', $this->options); 60 | $this->assertInstanceOf('\Apix\Cache\PsrCache\Pool', $pool); 61 | 62 | $pool = Cache\Factory::getPool('Array', $this->options); 63 | $this->assertInstanceOf('\Apix\Cache\PsrCache\Pool', $pool); 64 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 65 | } 66 | 67 | public function testPoolFromStringMixedCase() 68 | { 69 | $pool = Cache\Factory::getPool('arRay', $this->options); 70 | $this->assertInstanceOf('\Apix\Cache\PsrCache\Pool', $pool); 71 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 72 | } 73 | 74 | public function testPoolFromArray() 75 | { 76 | $pool = Cache\Factory::getPool(array(), $this->options); 77 | $this->assertInstanceOf('\Apix\Cache\PsrCache\Pool', $pool); 78 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 79 | } 80 | 81 | public function testTaggablePoolFromString() 82 | { 83 | $pool = Cache\Factory::getPool('ArrayObject', $this->options, true); 84 | $this->assertInstanceOf('\Apix\Cache\PsrCache\TaggablePool', $pool); 85 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 86 | } 87 | 88 | public function testGetTaggablePool() 89 | { 90 | $pool = Cache\Factory::getTaggablePool(array(), $this->options, true); 91 | $this->assertInstanceOf('\Apix\Cache\PsrCache\TaggablePool', $pool); 92 | $this->assertInstanceOf('\Apix\Cache\Runtime', $pool->getCacheAdapter()); 93 | } 94 | 95 | /** 96 | * @expectedException \Apix\Cache\Exception 97 | */ 98 | public function testGetPoolThrowsApixCacheException() 99 | { 100 | $adapter = new Cache\Runtime(new \StdClass, $this->options); 101 | Cache\Factory::getPool($adapter); 102 | } 103 | 104 | public function providerAsPerExample() 105 | { 106 | return array( 107 | 'pdo client' => array(new \PDO('sqlite::memory:'), 'Pdo\Sqlite'), 108 | 'files' => array('files', 'Files'), 109 | 'Files adapter' => array(new Cache\Files(), 'Files'), 110 | 'directory' => array('Directory', 'Directory'), 111 | 'Directory adapter' => array(new Cache\Directory(), 'Directory'), 112 | 'apc' => array('apc', 'Apc'), 113 | 'apcu' => array('apcu', 'Apcu'), 114 | 'runtime' => array('runtime', 'Runtime'), 115 | 'array' => array(array(), 'Runtime'), 116 | ); 117 | } 118 | 119 | /** 120 | * Regression test for bug GH#12 121 | * 122 | * @link https://github.com/frqnck/apix-cache/issues/12 123 | * "'Files' and 'Directory' are not listed as clients in the Factory" 124 | * @group regression 125 | * @dataProvider providerAsPerExample 126 | */ 127 | public function testBugFactoryExample($backend, $expected) 128 | { 129 | $pool = Cache\Factory::getPool( $backend ); 130 | $this->assertInstanceOf( 131 | '\Apix\Cache\\' . $expected, 132 | $pool->getCacheAdapter() 133 | ); 134 | } 135 | 136 | /** 137 | * Regression test for bug GH#13 138 | * 139 | * @link https://github.com/frqnck/apix-cache/issues/13 140 | * "TaggablePool and Pool overrides prefix_key and prefix_tag options 141 | * with hardcoded values" 142 | * @group regression 143 | * @see PsrCache\PoolTest\testBug13() 144 | * @see PsrCache\TaggablePoolTest\testBug13() 145 | */ 146 | public function testBug13() 147 | { 148 | $pool = Cache\Factory::getPool(array(), $this->options, true); 149 | $this->assertSame( 150 | $this->options['prefix_key'], 151 | $pool->getCacheAdapter()->getOption('prefix_key') 152 | ); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /tests/FilesTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | class FilesTest extends GenericTestCase 18 | { 19 | /** @var null|Cache\Files **/ 20 | protected $cache = null; 21 | 22 | /** @var string **/ 23 | protected $dir = null; 24 | 25 | public function setUp() 26 | { 27 | $this->dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'apix-cache-unittest'; 28 | 29 | $this->cache = new Cache\Files( 30 | $this->options + array('directory' => $this->dir) 31 | ); 32 | } 33 | 34 | public function tearDown() 35 | { 36 | if (null !== $this->cache) { 37 | $this->cache->flush(); 38 | unset($this->cache); 39 | 40 | rmdir($this->dir); 41 | } 42 | } 43 | 44 | public function testCorrupted() 45 | { 46 | $this->assertTrue($this->cache->save('data', 'id')); 47 | $encoded = base64_encode($this->cache->mapKey('id')); 48 | 49 | file_put_contents($this->cache->getOption('directory').DIRECTORY_SEPARATOR.$encoded, ''); 50 | $this->assertNull($this->cache->loadKey('id')); 51 | 52 | file_put_contents($this->cache->getOption('directory').DIRECTORY_SEPARATOR.$encoded, ' '); 53 | $this->assertNull($this->cache->loadKey('id')); 54 | 55 | file_put_contents($this->cache->getOption('directory').DIRECTORY_SEPARATOR.$encoded, ' '.PHP_EOL); 56 | $this->assertNull($this->cache->loadKey('id')); 57 | 58 | file_put_contents($this->cache->getOption('directory').DIRECTORY_SEPARATOR.$encoded, PHP_EOL); 59 | $this->assertNull($this->cache->loadKey('id')); 60 | } 61 | 62 | public function testExpired() 63 | { 64 | // 1 second ttl 65 | $this->cache->save('data', 'id1', null, 1); 66 | sleep(2); 67 | $this->assertNull($this->cache->loadKey('id1')); 68 | 69 | $this->cache->save('data', 'id2', null, 3); 70 | sleep(1); 71 | $this->assertEquals('data', $this->cache->loadKey('id2')); 72 | 73 | } 74 | 75 | public function testFlushAll() 76 | { 77 | $this->cache->save('testdata1', 'id1', array('tag1', 'tag2')); 78 | $this->cache->save('testdata2', 'id2', array('tag3', 'tag4')); 79 | $this->assertEquals(2, $this->getDirectoryFileCount($this->dir)); 80 | 81 | $this->cache->flush(true); 82 | $this->assertEquals(0, $this->getDirectoryFileCount($this->dir)); 83 | } 84 | 85 | /** 86 | * Regression test for pull request GH#17 87 | * 88 | * @link https://github.com/frqnck/apix-cache/pull/17/files 89 | * "File and Directory Adapters @clean returns when fails to find a 90 | * key within a tag" 91 | * @see DirectoryTest\testPullRequest17() 92 | * @group pr 93 | */ 94 | public function testPullRequest17() 95 | { 96 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')); 97 | 98 | $this->assertNotNull($this->cache->loadTag('tag2')); 99 | $this->assertTrue($this->cache->clean(array('non-existant', 'tag2'))); 100 | $this->assertNull($this->cache->loadTag('tag2')); 101 | 102 | // for good measure. 103 | $this->assertFalse($this->cache->clean(array('tag2'))); 104 | } 105 | 106 | private function getDirectoryFileCount($folder){ 107 | $iterator = new \FilesystemIterator($folder, \FilesystemIterator::SKIP_DOTS); 108 | return iterator_count($iterator); 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /tests/GenericTestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | class GenericTestCase extends TestCase 16 | { 17 | 18 | public function testLoadKeyReturnsNullWhenInexistant() 19 | { 20 | $this->assertNull($this->cache->loadKey('id')); 21 | } 22 | 23 | public function testLoadTagReturnsNullWhenInexistant() 24 | { 25 | $this->assertNull($this->cache->loadTag('id')); 26 | } 27 | 28 | public function testSaveAndLoadWithString() 29 | { 30 | $this->assertTrue($this->cache->save('data', 'id')); 31 | $this->assertEquals('data', $this->cache->loadKey('id')); 32 | $this->assertEquals('data', $this->cache->load('id')); 33 | } 34 | 35 | public function testSaveAndLoadWithArray() 36 | { 37 | $data = array('foo' => 'bar'); 38 | $this->assertTrue($this->cache->save($data, 'id')); 39 | 40 | $check = $this->cache->loadKey('id'); 41 | if (is_a($check, 'ArrayObject')) $check = $check->getArrayCopy(); 42 | $this->assertEquals($data, $check); 43 | 44 | $check = $this->cache->load('id'); 45 | if (is_a($check, 'ArrayObject')) $check = $check->getArrayCopy(); 46 | $this->assertEquals($data, $check); 47 | } 48 | 49 | public function testSaveAndLoadWithObject() 50 | { 51 | $data = new \stdClass(); 52 | $this->assertTrue($this->cache->save($data, 'id')); 53 | $this->assertEquals($data, $this->cache->loadKey('id')); 54 | $this->assertEquals($data, $this->cache->load('id')); 55 | } 56 | 57 | public function testDeleteInexistantReturnsFalse() 58 | { 59 | $this->assertFalse($this->cache->delete('Inexistant')); 60 | } 61 | 62 | public function testDelete() 63 | { 64 | $this->assertTrue( 65 | $this->cache->save('foo value', 'foo') 66 | && $this->cache->save('bar value', 'bar') 67 | ); 68 | 69 | $this->assertTrue($this->cache->delete('foo')); 70 | $this->assertFalse($this->cache->delete('foo')); 71 | 72 | $this->assertNull($this->cache->loadKey('foo')); 73 | } 74 | 75 | public function testTllFromSave() 76 | { 77 | $this->assertFalse($this->cache->getTtl('non-existant')); 78 | 79 | $this->assertTrue($this->cache->save('data', 'id')); 80 | $this->assertEquals(0, $this->cache->getTtl('id'), 81 | "Expiration should be set to 0 (for ever) by default." 82 | ); 83 | 84 | $this->assertTrue($this->cache->save('data', 'id', null, 3600)); 85 | $this->assertEquals(3600, $this->cache->getTtl('id'), null, 5); 86 | // $this->assertLessThanOrEqual(3600, $this->cache->getTtl('id')); 87 | } 88 | 89 | public function testTllFromLoad() 90 | { 91 | $this->assertFalse($this->cache->getTtl('non-existant')); 92 | 93 | $this->assertNull($this->cache->load('id')); 94 | $this->assertEquals(0, $this->cache->getTtl('id'), 95 | "Expiration should be set to 0 (for ever) by default." 96 | ); 97 | 98 | $this->assertTrue($this->cache->save('data', 'id', null, 3600)); 99 | $this->assertEquals('data', $this->cache->load('id')); 100 | $this->assertEquals(3600, $this->cache->getTtl('id'), null, 5); 101 | // $this->assertLessThanOrEqual(3600, $this->cache->getTtl('id')); 102 | } 103 | 104 | //// 105 | // The tests belowe are tags related 106 | //// 107 | 108 | public function testSaveWithTagDisabled() 109 | { 110 | $this->cache->setOptions(array('tag_enable' => false)); 111 | 112 | $this->assertTrue( 113 | $this->cache->save('data', 'id', array('tag1', 'tag2')) 114 | ); 115 | $this->assertNull($this->cache->loadTag('tag1')); 116 | } 117 | 118 | /** 119 | * @group testme 120 | */ 121 | public function testSaveWithJustOneSingularTag() 122 | { 123 | $this->assertTrue($this->cache->save('data', 'id', array('tag'))); 124 | $ids = array($this->cache->mapKey('id')); 125 | 126 | $this->assertEquals($ids, $this->cache->loadTag('tag')); 127 | $this->assertEquals($ids, $this->cache->load('tag', 'tag')); 128 | } 129 | 130 | public function testSaveManyTags() 131 | { 132 | $this->assertTrue( 133 | $this->cache->save('data', 'id', array('tag1', 'tag2')) 134 | ); 135 | $ids = array($this->cache->mapKey('id')); 136 | 137 | $this->assertEquals($ids, $this->cache->loadTag('tag2')); 138 | $this->assertEquals($ids, $this->cache->load('tag2', 'tag')); 139 | } 140 | 141 | public function testSaveWithOverlappingTags() 142 | { 143 | $this->assertTrue( 144 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 145 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 146 | ); 147 | 148 | $ids = $this->cache->loadTag('tag2'); 149 | $this->assertTrue(count($ids) == 2); 150 | $this->assertContains($this->cache->mapKey('id1'), $ids); 151 | $this->assertContains($this->cache->mapKey('id2'), $ids); 152 | } 153 | 154 | public function testClean() 155 | { 156 | $this->assertTrue( 157 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 158 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3', 'tag4')) 159 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 160 | ); 161 | 162 | $this->assertTrue($this->cache->clean(array('tag4'))); 163 | $this->assertFalse($this->cache->clean(array('tag4'))); 164 | $this->assertFalse($this->cache->clean(array('non-existant'))); 165 | 166 | $this->assertNull($this->cache->loadKey('id2')); 167 | $this->assertNull($this->cache->loadKey('id3')); 168 | $this->assertNull($this->cache->loadTag('tag4')); 169 | $this->assertEquals('data1', $this->cache->loadKey('id1')); 170 | } 171 | 172 | public function testDeleteAlsoRemoveTags() 173 | { 174 | $this->assertTrue( 175 | $this->cache->save('foo value', 'foo', array('foo_tag', 'all_tag')) 176 | && $this->cache->save('bar value', 'bar', array('bar_tag', 'all_tag')) 177 | ); 178 | 179 | $this->assertContains( 180 | $this->cache->mapKey('foo'), $this->cache->loadTag('foo_tag') 181 | ); 182 | $this->assertTrue($this->cache->delete('foo')); 183 | $this->assertNull($this->cache->loadKey('foo')); 184 | 185 | $this->assertNull($this->cache->loadTag('foo_tag')); 186 | 187 | $this->assertContains( 188 | $this->cache->mapKey('bar'), $this->cache->loadTag('all_tag') 189 | ); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /tests/Indexer/ApcIndexerTest_OFF.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Indexer; 14 | 15 | use Apix\Cache, 16 | Apix\Cache\Indexer; 17 | 18 | class ApcIndexerTest extends GenericIndexerTestCase 19 | { 20 | protected $cache, $indexer; 21 | 22 | public function setUp() 23 | { 24 | $this->skipIfMissing('apc'); 25 | 26 | if (!ini_get('apc.enable_cli')) { 27 | self::markTestSkipped( 28 | 'apc.enable_cli MUST be enabled in order to run this unit test' 29 | ); 30 | } 31 | 32 | $this->cache = new Cache\Apc($this->options); 33 | 34 | $this->indexer = new Indexer\ApcIndexer($this->indexKey, $this->cache); 35 | } 36 | 37 | public function tearDown() 38 | { 39 | if (null !== $this->cache) { 40 | $this->cache->flush(true); 41 | unset($this->cache, $this->indexer); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tests/Indexer/GenericIndexerTestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Indexer; 14 | 15 | use Apix\Cache\tests\TestCase; 16 | 17 | class GenericIndexerTestCase extends TestCase 18 | { 19 | protected $indexKey = 'indexKey'; 20 | 21 | public function testAddOneElement() 22 | { 23 | $this->assertTrue( $this->indexer->add('foo') ); 24 | $this->assertSame(array('foo'), $this->indexer->load()); 25 | } 26 | 27 | public function testAddManyElements() 28 | { 29 | $this->assertTrue( $this->indexer->add('foo') ); 30 | $this->assertTrue( $this->indexer->add(array('bar', 'baz'))); 31 | $this->assertSame(array('foo', 'bar', 'baz'), $this->indexer->load()); 32 | } 33 | 34 | public function testRemoveOneElement() 35 | { 36 | $this->assertNull($this->indexer->load()); 37 | 38 | $this->assertTrue( $this->indexer->add(array('foo', 'bar')) ); 39 | $this->assertSame(array('foo', 'bar'), $this->indexer->load()); 40 | 41 | $this->assertTrue( $this->indexer->remove('bar') ); 42 | $this->assertSame(array('foo'), $this->indexer->load()); 43 | } 44 | 45 | public function testRemoveManyElements() 46 | { 47 | $items = array('foo', 'bar', 'baz'); 48 | $this->assertNull($this->indexer->load()); 49 | 50 | $this->assertTrue( $this->indexer->add($items) ); 51 | $this->assertSame($items, $this->indexer->load()); 52 | 53 | $this->assertTrue( $this->indexer->Remove(array('foo', 'bar')) ); 54 | $this->assertSame(array('baz'), $this->indexer->load()); 55 | } 56 | 57 | public function testGetName() 58 | { 59 | $this->assertSame($this->indexKey, $this->indexer->getName()); 60 | } 61 | 62 | public function testGetItems() 63 | { 64 | $this->assertSame(array(), $this->indexer->getItems()); 65 | 66 | // $items = array('foo', 'bar', 'baz'); 67 | // $this->assertTrue( $this->indexer->add($items) ); 68 | // $this->assertSame($items, $this->indexer->getItems()); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /tests/Indexer/MemcachedIndexerTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Indexer; 14 | 15 | use Apix\Cache, 16 | Apix\Cache\Indexer; 17 | 18 | class MemcachedIndexerTest extends GenericIndexerTestCase 19 | { 20 | const HOST = '127.0.0.1'; 21 | const PORT = 11211; 22 | const AUTH = null; 23 | 24 | protected $cache, $memcached, $indexer; 25 | 26 | public function getMemcached() 27 | { 28 | try { 29 | $m = new \Memcached(); 30 | $m->addServer(self::HOST, self::PORT); 31 | 32 | $stats = $m->getStats(); 33 | $host = self::HOST.':'.self::PORT; 34 | if($stats[$host]['pid'] == -1) 35 | throw new \Exception( 36 | sprintf('Unable to reach a memcached server on %s', $host) 37 | ); 38 | 39 | } catch (\Exception $e) { 40 | $this->markTestSkipped( $e->getMessage() ); 41 | } 42 | 43 | return $m; 44 | } 45 | 46 | public function setUp() 47 | { 48 | $this->skipIfMissing('memcached'); 49 | $this->memcached = $this->getMemcached(); 50 | $this->cache = new Cache\Memcached($this->memcached, $this->options); 51 | 52 | $this->indexer = new Indexer\MemcachedIndexer($this->indexKey, $this->cache); 53 | } 54 | 55 | public function tearDown() 56 | { 57 | if (null !== $this->cache) { 58 | $this->cache->flush(true); 59 | $this->memcached->quit(); 60 | unset($this->cache, $this->memcached, $this->indexer); 61 | } 62 | } 63 | 64 | public function testLoadDoesPurge() 65 | { 66 | $keys = range(1, 101); 67 | $this->assertTrue($this->indexer->add('a')); 68 | $this->assertTrue($this->indexer->remove($keys)); 69 | 70 | $keyStr = implode(' -', $keys); 71 | $this->assertEquals( 72 | 'a -' . $keyStr . ' ', $this->cache->get($this->indexKey) 73 | ); 74 | 75 | $this->assertEquals(array('a'), $this->indexer->load() ); 76 | 77 | $this->assertNull($this->cache->get($this->indexKey)); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /tests/MemcachedTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | class MemcachedTest extends GenericTestCase 18 | { 19 | const HOST = '127.0.0.1'; 20 | const PORT = 11211; 21 | const AUTH = null; 22 | 23 | /** @var \Apix\Cache\AbstractCache */ 24 | protected $cache; 25 | 26 | /** @var \Memcached */ 27 | protected $memcached; 28 | 29 | protected $options = array( 30 | 'prefix_key' => 'key_', 31 | 'prefix_tag' => 'tag_', 32 | 'prefix_idx' => 'idx_', 33 | 'serializer' => 'php' 34 | ); 35 | 36 | public function getMemcached() 37 | { 38 | try { 39 | $m = new \Memcached(); 40 | $m->addServer(self::HOST, self::PORT); 41 | 42 | $stats = $m->getStats(); 43 | $host = self::HOST.':'.self::PORT; 44 | if($stats[$host]['pid'] == -1) 45 | throw new \Exception( 46 | sprintf('Unable to reach a memcached server on %s', $host) 47 | ); 48 | 49 | } catch (\Exception $e) { 50 | $this->markTestSkipped( $e->getMessage() ); 51 | } 52 | 53 | return $m; 54 | } 55 | 56 | public function setUp() 57 | { 58 | $this->skipIfMissing('memcached'); 59 | $this->memcached = $this->getMemcached(); 60 | $this->cache = new Cache\Memcached($this->memcached, $this->options); 61 | } 62 | 63 | public function tearDown() 64 | { 65 | if (null !== $this->cache) { 66 | $this->cache->flush(true); 67 | $this->memcached->quit(); 68 | unset($this->cache, $this->memcached); 69 | } 70 | } 71 | 72 | public function _commonMemcachedData() 73 | { 74 | return $this->assertTrue( 75 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 76 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3', 'tag4')) 77 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 78 | ); 79 | $this->assertSame('data3', $this->cache->loadKey('id3')); 80 | } 81 | 82 | public function testSaveIsUniqueAndOverwrite() 83 | { 84 | $this->assertTrue( 85 | $this->cache->save('bar1', 'foo') 86 | && $this->cache->save('bar2', 'foo') 87 | ); 88 | $this->assertEquals('bar2', $this->cache->loadKey('foo')); 89 | } 90 | 91 | public function testFlushNamespace() 92 | { 93 | $this->_commonMemcachedData(); 94 | 95 | $otherMemcached = $this->getMemcached(); 96 | $otherMemcached->add('foo', 'bar'); 97 | 98 | $this->assertTrue($this->cache->flush(), "Flush the namespace"); 99 | 100 | $this->assertEquals('bar', $otherMemcached->get('foo')); 101 | 102 | $this->assertNull($this->cache->loadKey('id3')); 103 | $this->assertNull($this->cache->loadTag('tag1')); 104 | } 105 | 106 | public function testFlushIncrementsTheNamspaceIndex() 107 | { 108 | $this->_commonMemcachedData(); 109 | $ns = $this->cache->getOption('prefix_nsp'); 110 | 111 | $this->assertEquals($ns.'1_', $this->cache->getNamespace()); 112 | $this->assertTrue($this->cache->flush(), "Flush the namespace"); 113 | $this->assertEquals($ns.'2_', $this->cache->getNamespace()); 114 | } 115 | 116 | public function testFlushAll() 117 | { 118 | $this->_commonMemcachedData(); 119 | 120 | $this->getMemcached()->add('foo', 'bar'); 121 | 122 | $this->assertTrue($this->cache->flush(true)); 123 | $this->assertNull($this->cache->get('foo')); 124 | $this->assertNull($this->cache->loadKey('id3')); 125 | $this->assertNull($this->cache->loadTag('tag1')); 126 | } 127 | 128 | public function testDeleteWithTagDisabled() 129 | { 130 | $this->cache->setOptions(array('tag_enable' => false)); 131 | 132 | $this->assertTrue( 133 | $this->cache->save('data', 'id', array('tag1', 'tag2')) 134 | ); 135 | 136 | $this->assertTrue($this->cache->delete('id')); 137 | $this->assertNull($this->cache->loadTag('tag1')); 138 | 139 | $idxKey = $this->cache->mapIdx('id'); 140 | $this->assertNull($this->cache->getIndex($idxKey)->load()); 141 | } 142 | 143 | /** 144 | * @group encours 145 | */ 146 | public function testDelete() 147 | { 148 | $tags = array('tag1', 'tag2'); 149 | $this->assertTrue($this->cache->save('data', 'id', $tags)); 150 | 151 | $this->assertSame( 152 | array($this->cache->mapKey('id')), $this->cache->loadTag('tag1'), 153 | 'tag1 isset' 154 | ); 155 | 156 | // check the idx isset 157 | $indexer = $this->cache->getIndex($this->cache->mapIdx('id')); 158 | $this->assertSame( $tags, $indexer->load(), 'idx_id isset'); 159 | 160 | $this->assertTrue($this->cache->delete('id')); 161 | $this->assertFalse($this->cache->delete('id')); 162 | 163 | $this->assertNull($this->cache->loadTag('tag1'), 'tag1 !isset'); 164 | $this->assertNull($indexer->load(), 'idx_id !isset'); 165 | } 166 | 167 | public function testIndexing() 168 | { 169 | $this->assertTrue( 170 | $this->cache->save('data1', 'id1', array('tag1', 'tag2', 'tag3')) 171 | ); 172 | $idx = $this->cache->mapIdx('id1'); 173 | $this->assertEquals( 174 | 'tag1 tag2 tag3 ', $this->cache->get($idx) 175 | ); 176 | $this->assertTrue( 177 | $this->cache->getIndex($idx)->remove(array('tag3')) 178 | // $this->cache->saveIndex($idx, array('tag3'), '-') 179 | ); 180 | $this->assertEquals( 181 | 'tag1 tag2 tag3 -tag3 ', $this->cache->get($idx) 182 | ); 183 | } 184 | 185 | public function OFF_testShortTtlDoesExpunge() 186 | { 187 | $this->assertTrue( 188 | $this->cache->save('ttl-1', 'ttlId', array('someTags!'), -1) 189 | ); 190 | 191 | // How to forcibly run garbage collection? 192 | // $this->cache->db->command(array( 193 | // 'reIndex' => 'cache' 194 | // )); 195 | 196 | $this->assertNull( $this->cache->load('ttlId') ); 197 | } 198 | 199 | public function testSetSerializerToNull() 200 | { 201 | $this->cache->setSerializer(null); 202 | $this->assertSame( 203 | \Memcached::SERIALIZER_PHP, $this->cache->getSerializer() 204 | ); 205 | } 206 | 207 | public function testSetSerializerToPhp() 208 | { 209 | $this->cache->setSerializer('php'); 210 | $this->assertSame( 211 | \Memcached::SERIALIZER_PHP, $this->cache->getSerializer() 212 | ); 213 | } 214 | 215 | public function testSetSerializerToJson() 216 | { 217 | if (defined('\Memcached::SERIALIZER_JSON') 218 | && \Memcached::HAVE_JSON 219 | ) { 220 | $this->cache->setSerializer('json'); 221 | $this->assertSame( 222 | \Memcached::SERIALIZER_JSON, $this->cache->getSerializer() 223 | ); 224 | } 225 | } 226 | 227 | public function testSetSerializerToJsonArray() 228 | { 229 | if (defined('\Memcached::SERIALIZER_JSON_ARRAY') 230 | && \Memcached::HAVE_JSON 231 | ) { 232 | $this->cache->setSerializer('json_array'); 233 | $this->assertSame( 234 | \Memcached::SERIALIZER_JSON_ARRAY, $this->cache->getSerializer() 235 | ); 236 | } 237 | } 238 | 239 | public function testSetSerializerToIgbinary() 240 | { 241 | if (defined('\Memcached::SERIALIZER_IGBINARY') 242 | && \Memcached::HAVE_IGBINARY 243 | ) { 244 | $this->cache->setSerializer('igBinary'); 245 | $this->assertSame( 246 | \Memcached::SERIALIZER_IGBINARY, $this->cache->getSerializer() 247 | ); 248 | } 249 | } 250 | 251 | public function testSetSerializerToMsgpack() 252 | { 253 | if (defined('\Memcached::SERIALIZER_MSGPACK') 254 | && \Memcached::HAVE_MSGPACK 255 | ) { 256 | $this->cache->setSerializer('msgpack'); 257 | $this->assertSame( 258 | \Memcached::SERIALIZER_MSGPACK, $this->cache->getSerializer() 259 | ); 260 | } 261 | } 262 | 263 | public function testIncrement() 264 | { 265 | $this->options = array('tag_enable' => true); 266 | 267 | $this->cache = new Cache\Memcached($this->memcached, $this->options); 268 | 269 | $this->assertNull($this->cache->get('testInc')); 270 | $this->assertEquals(1, $this->cache->increment('testInc')); 271 | $this->assertEquals(1, $this->cache->get('testInc')); 272 | $this->assertEquals(2, $this->cache->increment('testInc')); 273 | $this->assertEquals(2, $this->cache->get('testInc')); 274 | } 275 | 276 | // public function testIncrementWithBinaryProtocole() 277 | // { 278 | // $m = $this->getMemcached(); 279 | // $m->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); 280 | 281 | // $opts = array('tag_enable' => false); 282 | // $cache = new Memcached($m, $opts); 283 | 284 | // $this->assertEquals(1, $cache->increment('testInc')); 285 | // $this->assertEquals(1, $cache->get('testInc')); 286 | 287 | // $this->assertEquals(2, $cache->increment('testInc')); 288 | // $this->assertEquals(2, $cache->get('testInc')); 289 | // } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /tests/MongoTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | /** 18 | * MongoTest 19 | * 20 | * @package Apix\Cache 21 | * @author Franck Cassedanne 22 | */ 23 | class MongoTest extends GenericTestCase 24 | { 25 | protected $cache, $mongo; 26 | 27 | public function setUp() 28 | { 29 | if (phpversion() >= '7.0.0' || defined('HHVM_VERSION')) { 30 | $this->skipIfMissing('mongodb'); 31 | $class = '\MongoDB\Client'; 32 | } else { 33 | $this->skipIfMissing('mongo'); 34 | $class = '\MongoClient'; 35 | } 36 | 37 | try { 38 | $this->mongo = new $class(); 39 | } catch (\Exception $e) { 40 | $this->markTestSkipped( $e->getMessage() ); 41 | } 42 | 43 | $this->cache = new Cache\Mongo($this->mongo, $this->options); 44 | } 45 | 46 | public function tearDown() 47 | { 48 | if (null !== $this->cache) { 49 | $this->cache->flush(); 50 | if (method_exists($this->mongo, 'close')) 51 | $this->mongo->close(); 52 | unset($this->cache, $this->mongo); 53 | } 54 | } 55 | 56 | public function testSaveIsUnique() 57 | { 58 | $this->assertTrue($this->cache->save('bar1', 'foo')); 59 | $this->assertTrue($this->cache->save('bar2', 'foo')); 60 | 61 | $this->assertEquals('bar2', $this->cache->loadKey('foo')); 62 | 63 | $this->assertEquals(1, $this->cache->count('foo') ); 64 | } 65 | 66 | public function testFlushCacheOnly() 67 | { 68 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')); 69 | $this->cache->save('data2', 'id2', array('tag2', 'tag3')); 70 | $this->cache->save('data3', 'id3', array('tag3', 'tag4')); 71 | 72 | $foo = array('foo' => 'bar'); 73 | $this->cache->collection->insert($foo); 74 | 75 | $this->assertTrue($this->cache->flush()); 76 | 77 | $check = $this->cache->collection->findOne(array('foo'=>'bar')); 78 | if (is_a($check, 'ArrayObject')) $check = $check->getArrayCopy(); 79 | $this->assertEquals($foo, $check); 80 | 81 | $this->assertNull($this->cache->loadKey('id3')); 82 | $this->assertNull($this->cache->loadTag('tag1')); 83 | } 84 | 85 | public function testFlushAll() 86 | { 87 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')); 88 | $this->cache->save('data2', 'id2', array('tag2', 'tag3')); 89 | $this->cache->save('data3', 'id3', array('tag3', 'tag4')); 90 | 91 | $this->cache->collection->insert(array('key' => 'foobar')); 92 | 93 | $this->assertTrue($this->cache->flush(true)); 94 | $this->assertNull($this->cache->collection->findOne(array('key'))); 95 | 96 | $this->assertNull($this->cache->loadKey('id3')); 97 | $this->assertNull($this->cache->loadTag('tag1')); 98 | } 99 | 100 | public function testShortTtlDoesExpunge() 101 | { 102 | $this->assertTrue( 103 | $this->cache->save('ttl-1', 'ttlId', array('someTags!'), -1) 104 | ); 105 | 106 | // How to forcibly run garbage collection? 107 | // $this->cache->db->command(array( 108 | // 'reIndex' => 'cache' 109 | // )); 110 | 111 | $this->assertNull( $this->cache->loadKey('ttlId') ); 112 | } 113 | 114 | public function testArrayWithArbitraryKeys() 115 | { 116 | $arr = array('A:B' => 'test', 'A&B' => 'test2', 'A.B' => 'test3'); 117 | 118 | $this->assertTrue($this->cache->save($arr, 'array', array(), 5)); 119 | $this->assertEquals($arr, $this->cache->loadKey('array')); 120 | } 121 | 122 | /** 123 | * @expectedException InvalidArgumentException 124 | */ 125 | public function testThrowAnInvalidArgumentException() 126 | { 127 | new Cache\Mongo(new \stdClass); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /tests/PdoTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | use Apix\Cache\AbstractPdo; 17 | 18 | /** 19 | * @covers Apix\Cache\AbstractPdo 20 | * @covers Apix\Cache\Pdo\Sqlite 21 | * @covers Apix\Cache\Pdo\Mysql 22 | * @covers Apix\Cache\Pdo\Pgsql 23 | * @covers Apix\Cache\Pdo\Sql1999 24 | */ 25 | class PdoTest extends GenericTestCase 26 | { 27 | /** @var AbstractPdo */ 28 | protected $cache; 29 | protected $pdo; 30 | 31 | protected $options = array( 32 | 'db_name' => 'apix_tests', 33 | 'db_table' => 'cache' 34 | ); 35 | 36 | public function pdoProvider() 37 | { 38 | $dbs = array( 39 | 'sqlite' => array( 40 | 'pdo_sqlite', 41 | function () { 42 | return new \PDO('sqlite::memory:'); 43 | }, 44 | 'Sqlite' 45 | ), 46 | 'mysql' => array( 47 | 'pdo_mysql', 48 | function () { 49 | return new \PDO( 50 | 'mysql:dbname=apix_tests;host=127.0.0.1', 'root' 51 | ); 52 | }, 53 | 'Mysql' 54 | ), 55 | 'pgsql' => array( 56 | 'pdo_pgsql', 57 | function () { 58 | return new \PDO( 59 | 'pgsql:dbname=apix_tests;host=127.0.0.1', 'postgres' 60 | ); 61 | }, 62 | 'Pgsql' 63 | ), 64 | 'sql1999' => array( 65 | 'pdo_sqlite', 66 | function () { 67 | return new \PDO('sqlite::memory:'); 68 | }, 69 | 'Sql1999' 70 | ) 71 | ); 72 | $DB = getenv('DB') ? getenv('DB') : 'sqlite'; 73 | 74 | if (in_array($DB, array_keys($dbs))) { 75 | return $dbs[$DB]; 76 | } 77 | 78 | $this->markTestSkipped("Unsupported PDO DB ($DB) environment."); 79 | } 80 | 81 | public function setUp() 82 | { 83 | list($ext_name, $dbh, $class) = $this->pdoProvider(); 84 | 85 | $this->skipIfMissing('pdo'); 86 | $this->skipIfMissing($ext_name); 87 | 88 | try { 89 | $this->pdo = $dbh(); 90 | } catch (\Exception $e) { 91 | $this->markTestSkipped( $e->getMessage() ); 92 | } 93 | 94 | $this->classname = 'Apix\\Cache\\Pdo\\' . $class; 95 | $this->cache = new $this->classname($this->pdo, $this->options); 96 | } 97 | 98 | public function tearDown() 99 | { 100 | if (null !== $this->cache) { 101 | $this->cache->flush(true); 102 | unset($this->cache, $this->pdo, $this->classname); 103 | } 104 | } 105 | 106 | public function testSaveIsUnique() 107 | { 108 | $this->assertTrue($this->cache->save('bar_1', 'foo')); 109 | $this->assertEquals('bar_1', $this->cache->load('foo')); 110 | 111 | $this->assertTrue($this->cache->save('bar_2', 'foo')); 112 | $this->assertEquals('bar_2', $this->cache->load('foo')); 113 | 114 | // $this->assertEquals(1, $this->cache->getAdapter()->rowCount() ); 115 | } 116 | 117 | public function testFlushCacheOnly() 118 | { 119 | $this->assertTrue( 120 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 121 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 122 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 123 | ); 124 | // $foo = array('foo' => 'bar'); 125 | // $this->cache->getAdapter()->add($foo); 126 | 127 | $this->assertTrue($this->cache->flush()); 128 | 129 | // $this->assertEquals( 130 | // $foo, 131 | // $this->cache->getAdapter()->findOne(array('foo'=>'bar')) 132 | // ); 133 | 134 | $this->assertNull($this->cache->load('id3')); 135 | $this->assertNull($this->cache->load('tag1', 'tag')); 136 | } 137 | 138 | /** 139 | * With multiple caches in play, calling flush() should only remove the 140 | * entries for the cache instance it is called on. 141 | */ 142 | public function testFlushCacheOnly_secondCache() 143 | { 144 | $options = array( 145 | 'prefix_key' => 'second-cache-key:', // prefix cache keys 146 | 'prefix_tag' => 'second-cache-tag:', // prefix cache tags 147 | 'tag_enable' => true // wether to enable tagging 148 | ); 149 | 150 | /** @var AbstractPdo */ 151 | $localCache = new $this->classname($this->pdo, $options); 152 | 153 | $this->assertTrue( 154 | $this->cache->save('data1_1', 'id1_1', array('tag1_1', 'tag1_2')) && 155 | $this->cache->save('data1_2', 'id1_2', array('tag1_2', 'tag1_3')) && 156 | $this->cache->save('data1_3', 'id1_3', array('tag1_3', 'tag1_4')) 157 | ); 158 | 159 | $this->assertTrue( 160 | $localCache->save('data2_1', 'id2_1', array('tag2_1', 'tag2_2')) && 161 | $localCache->save('data2_2', 'id2_2', array('tag2_2', 'tag2_3')) && 162 | $localCache->save('data2_3', 'id2_3', array('tag2_3', 'tag2_4')) 163 | ); 164 | 165 | $result = $this->cache->flush(); 166 | $this->assertTrue($result); 167 | 168 | $this->assertNull($this->cache->load('id1_3')); 169 | $this->assertNull($this->cache->load('tag1_1', 'tag')); 170 | 171 | $this->assertNotNull($localCache->load('id2_3')); 172 | $this->assertNotNull($localCache->load('tag2_1', 'tag')); 173 | 174 | unset($localCache); 175 | } 176 | 177 | /** 178 | * 179 | */ 180 | public function testFlushAll() 181 | { 182 | $this->assertTrue( 183 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 184 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 185 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 186 | ); 187 | 188 | $this->assertTrue($this->cache->flush(true)); 189 | 190 | try { 191 | /* 192 | * Previously flush() would have dropped the cache table which 193 | * would result in a PDOException being thrown if the table were 194 | * subsequently be accessed. 195 | */ 196 | $this->assertNull($this->cache->load('id3')); 197 | $this->assertNull($this->cache->load('tag1', 'tag')); 198 | } 199 | catch (\PDOException $e) { 200 | /* Because the cache table is no longer dropped we should never 201 | * reach this point. 202 | */ 203 | $this->fail('PDOException should not have been thrown'); 204 | } 205 | } 206 | 207 | /** 208 | * With multiple caches in play, calling flush(true) should remove all of 209 | * the entries from the database regardless of which cache instance they 210 | * are associated with. 211 | */ 212 | public function testFlushAll_secondCache() 213 | { 214 | $options = array( 215 | 'prefix_key' => 'second-cache-key:', // prefix cache keys 216 | 'prefix_tag' => 'second-cache-tag:', // prefix cache tags 217 | 'tag_enable' => true // wether to enable tagging 218 | ); 219 | 220 | /** @var AbstractPdo */ 221 | $localCache = new $this->classname($this->pdo, $options); 222 | 223 | $this->assertTrue( 224 | $this->cache->save('data1_1', 'id1_1', array('tag1_1', 'tag1_2')) && 225 | $this->cache->save('data1_2', 'id1_2', array('tag1_2', 'tag1_3')) && 226 | $this->cache->save('data1_3', 'id1_3', array('tag1_3', 'tag1_4')) 227 | ); 228 | 229 | $this->assertTrue( 230 | $localCache->save('data2_1', 'id2_1', array('tag2_1', 'tag2_2')) && 231 | $localCache->save('data2_2', 'id2_2', array('tag2_2', 'tag2_3')) && 232 | $localCache->save('data2_3', 'id2_3', array('tag2_3', 'tag2_4')) 233 | ); 234 | 235 | $this->assertTrue($this->cache->flush(true)); 236 | 237 | $this->assertNull($this->cache->load('id1_3')); 238 | $this->assertNull($this->cache->load('tag1_1', 'tag')); 239 | 240 | $this->assertNull($localCache->load('id2_3')); 241 | $this->assertNull($localCache->load('tag2_1', 'tag')); 242 | 243 | unset($localCache); 244 | } 245 | 246 | public function testShortTtlDoesExpunge() 247 | { 248 | $this->assertTrue( 249 | $this->cache->save('ttl-1', 'ttlId', array('someTags!'), -1) 250 | ); 251 | 252 | $this->assertNull( $this->cache->load('ttlId') ); 253 | } 254 | 255 | public function testTtlSetToNull() 256 | { 257 | $this->assertTrue( 258 | $this->cache->save('ttl-null', 'ttlId', array('someTags!'), null) 259 | ); 260 | 261 | $this->assertEquals('ttl-null', $this->cache->load('ttlId') ); 262 | } 263 | 264 | public function testPurge() 265 | { 266 | $this->assertTrue( 267 | $this->cache->save('120s', 'id1', null, 120) 268 | && $this->cache->save('600s', 'id2', null, 600) 269 | ); 270 | $this->assertEquals('120s', $this->cache->load('id1')); 271 | $this->assertTrue($this->cache->purge(130)); 272 | $this->assertFalse($this->cache->purge()); 273 | $this->assertNull($this->cache->load('id1')); 274 | 275 | $this->assertEquals('600s', $this->cache->load('id2')); 276 | $this->assertTrue($this->cache->purge(630)); 277 | $this->assertNull($this->cache->load('id2')); 278 | } 279 | 280 | public function testcreateIndexTableReturnsFalse() 281 | { 282 | $this->assertFalse( $this->cache->createIndexTable('not-defined') ); 283 | } 284 | 285 | public function testGetDriverName() 286 | { 287 | if ($this->classname != 'Apix\Cache\Pdo\Sql1999') { 288 | $this->assertSame( 289 | $this->classname, 290 | 'Apix\\Cache\\Pdo\\' 291 | . Cache\AbstractPdo::getDriverName($this->pdo) 292 | ); 293 | } 294 | } 295 | 296 | /** 297 | * Regression test for pull request GH#28 298 | * 299 | * @link https://github.com/frqnck/apix-cache/pull/28 300 | * PDOException: SQLSTATE[23000]: Integrity constraint violation: 301 | * 1062 Duplicate entry 'apix-cache-key:same_key' for key 'PRIMARY' 302 | * @group pr 303 | */ 304 | public function testPullRequest28() 305 | { 306 | $this->cache->save('same_data', 'same_key'); 307 | $this->cache->save('same_data', 'same_key'); 308 | 309 | $this->assertEquals('same_data', $this->cache->load('same_key')); 310 | } 311 | 312 | /** 313 | * @requires PHP 7.3 314 | */ 315 | public function testCacheMiss_loadKey() 316 | { 317 | $cacheMissKey = 'CacheMissKey'; 318 | 319 | try { 320 | $result = $this->cache->loadKey($cacheMissKey); 321 | 322 | $this->assertNull($result); 323 | } catch (\Exception $e) { 324 | $this->fail('Exception should not have been thrown'); 325 | } 326 | } 327 | 328 | /** 329 | * @requires PHP 7.3 330 | */ 331 | public function testCacheMiss_loadTag() 332 | { 333 | $cacheMissTag = 'CacheMissTag'; 334 | 335 | try { 336 | $result = $this->cache->loadTag($cacheMissTag); 337 | 338 | $this->assertNull($result); 339 | } catch (\Exception $e) { 340 | $this->fail('Exception should not have been thrown'); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /tests/PsrCache/ItemTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\PsrCache; 14 | 15 | use Apix\Cache\tests\TestCase; 16 | use Apix\Cache\PsrCache\Item; 17 | 18 | class ItemTest extends TestCase 19 | { 20 | protected $item = null; 21 | 22 | public function setUp() 23 | { 24 | $this->item = new Item('foo'); 25 | } 26 | 27 | public function tearDown() 28 | { 29 | if (null !== $this->item) { 30 | unset($this->item); 31 | } 32 | } 33 | 34 | public function testItemConstructor() 35 | { 36 | $item = new Item('foo', 'bar'); 37 | $this->assertEquals('foo', $item->getKey()); 38 | $this->assertNull($item->get()); 39 | 40 | $this->assertFalse($item->isHit()); 41 | } 42 | 43 | public function testItemConstructorHittingCache() 44 | { 45 | $item = new Item('foo', 'bar', null, true); 46 | $this->assertEquals('foo', $item->getKey()); 47 | $this->assertEquals('bar', $item->get()); 48 | 49 | $this->assertTrue($item->isHit()); 50 | } 51 | 52 | public function testSetAndisHit() 53 | { 54 | $this->assertSame($this->item, $this->item->set('new foo value')); 55 | $this->assertFalse($this->item->isHit()); 56 | 57 | $this->assertSame($this->item, $this->item->setHit(true)); 58 | $this->assertSame('new foo value', $this->item->get()); 59 | } 60 | 61 | public function expiresAtProvider() 62 | { 63 | return array( 64 | 'DateTime' => array(new \DateTime('1 day'), '1 day', 86400), 65 | 'Zero to Now' => array(0, null, 0), // or 'now' (same) 66 | 'Integer' => array(999, '+999 seconds', 999), 67 | 'Null' => array(null, Item::DEFAULT_EXPIRATION, null), 68 | ); 69 | } 70 | 71 | /** 72 | * @dataProvider expiresAtProvider 73 | */ 74 | public function testExpiresAt($from, $to) 75 | { 76 | $this->assertSame($this->item, $this->item->expiresAt($from)); 77 | $date = new \DateTime($to); 78 | 79 | $expire = $this->item->getExpiration(); 80 | $this->assertInstanceOf('DateTime', $expire); 81 | 82 | $this->assertEquals( 83 | (int) $date->format('U'), $expire->format('U'), '', 10 84 | ); 85 | } 86 | 87 | /** 88 | * @dataProvider expiresAtProvider 89 | */ 90 | public function testGetTtlInSecond($from, $to, $sec) 91 | { 92 | if ($sec !== null) { 93 | $this->item->expiresAt($from); 94 | $this->assertEquals($sec, $this->item->getTtlInSecond(), '', 10); 95 | } 96 | } 97 | 98 | public function expiresAfterProvider() 99 | { 100 | return array( 101 | 'DateInterval' => array(new \DateInterval('P1Y'), 'now+1year'), 102 | 'Zero to Now' => array(0, null), // or 'now' (same) 103 | 'Integer' => array(999, '+999 seconds'), 104 | 'Null' => array(null, Item::DEFAULT_EXPIRATION), 105 | ); 106 | } 107 | 108 | /** 109 | * @dataProvider expiresAfterProvider 110 | */ 111 | public function testExpiresAfter($from, $to) 112 | { 113 | $this->assertSame($this->item, $this->item->expiresAfter($from)); 114 | $date = new \DateTime($to); 115 | 116 | $expire = $this->item->getExpiration(); 117 | $this->assertInstanceOf('DateTime', $expire); 118 | 119 | $this->assertEquals( 120 | (int) $date->format('U'), $expire->format('U'), '', 10 121 | ); 122 | } 123 | 124 | /** 125 | * @expectedException Apix\Cache\PsrCache\InvalidArgumentException 126 | */ 127 | public function testExpiresAtThrowAnException() 128 | { 129 | $this->item->expiresAt('string'); 130 | } 131 | 132 | /** 133 | * @expectedException Apix\Cache\PsrCache\InvalidArgumentException 134 | */ 135 | public function testExpiresAfterThrowAnException() 136 | { 137 | $this->item->expiresAfter('string'); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /tests/PsrCache/PoolTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\PsrCache; 14 | 15 | use Apix\Cache\tests\TestCase; 16 | 17 | use Apix\Cache, 18 | Apix\Cache\PsrCache\Pool; 19 | 20 | class PoolTest extends TestCase 21 | { 22 | protected $pool = null, $item = null; 23 | 24 | public function setUp() 25 | { 26 | $cache = new Cache\Runtime(); 27 | 28 | $this->pool = new Pool($cache); 29 | $this->item = $this->pool->getItem('foo')->set('foo value'); 30 | $this->assertTrue( $this->pool->save($this->item) ); 31 | } 32 | 33 | public function tearDown() 34 | { 35 | unset($this->pool, $this->item); 36 | } 37 | 38 | public function testGetItemWithNonExistantKey() 39 | { 40 | $item = $this->pool->getItem('non-existant'); 41 | $this->assertInstanceOf('Psr\Cache\CacheItemInterface', $item); 42 | } 43 | 44 | /** 45 | * The following characters are reserved for future extensions 46 | * and MUST NOT be supported by implementing libraries: 47 | * {}()/\@: 48 | * 49 | * @return array 50 | */ 51 | public static function _invalidKeyProvider() 52 | { 53 | return array( 54 | array('foo{bar'), 55 | array('foo}bar'), 56 | array('foo(bar'), 57 | array('foo)bar'), 58 | array('foo/bar'), 59 | array('foo\\bar'), 60 | array('foo@bar'), 61 | array('foo:bar'), 62 | 'null' => array( null ), 63 | 'boolean' => array( true ), 64 | 'integer' => array( 1 ), 65 | 'float' => array( 1.1 ) 66 | ); 67 | } 68 | 69 | /** 70 | * @dataProvider _invalidKeyProvider 71 | * @expectedException \Apix\Cache\PsrCache\InvalidArgumentException 72 | */ 73 | public function testGetItemThrowsException($key) 74 | { 75 | $this->pool->getItem($key); 76 | } 77 | 78 | public function testBasicSetAndGetOperations() 79 | { 80 | // Create the 'bar' item. 81 | $item = $this->pool->getItem('bar'); 82 | 83 | // Set a the 'bar' item value. 84 | $item->set('bar value'); 85 | $this->assertNull($item->get()); 86 | $this->pool->save($item); 87 | $this->assertEquals('bar value', $item); 88 | 89 | // Update the 'bar' item value. 90 | $item->set('new bar value'); 91 | $this->assertNull($item->get()); 92 | $this->pool->save($item); 93 | $this->assertEquals('new bar value', $item); 94 | } 95 | 96 | public function testGetItems() 97 | { 98 | $this->assertSame(array(), $this->pool->getItems()); 99 | 100 | $items = $this->pool->getItems(array('non-existant')); 101 | $this->assertInstanceOf( 102 | '\Psr\Cache\CacheItemInterface', $items['non-existant'] 103 | ); 104 | 105 | $items = $this->pool->getItems( array('foo') ); 106 | $this->assertEquals('foo value', $items['foo']); 107 | } 108 | 109 | public function testSave() 110 | { 111 | $item = $this->pool->getItem('baz')->set('foo value'); 112 | $this->assertFalse($item->isHit()); 113 | $this->assertTrue($this->pool->save($item)); 114 | } 115 | 116 | public function testClear() 117 | { 118 | $this->assertTrue($this->pool->clear()); 119 | 120 | $item = $this->pool->getItem('foo'); 121 | 122 | $this->assertFalse($item->isHit()); 123 | } 124 | 125 | public function testDeleteItems() 126 | { 127 | $this->assertTrue( 128 | $this->pool->deleteItems(array('foo', 'non-existant')) 129 | ); 130 | 131 | $this->assertFalse( $this->pool->hasItem('foo') ); 132 | } 133 | 134 | public function testDeleteItem() 135 | { 136 | $this->assertTrue( $this->pool->deleteItem('foo') ); 137 | 138 | $this->assertTrue( $this->pool->deleteItem('foo') ); 139 | $this->assertFalse( $this->pool->hasItem('foo') ); 140 | } 141 | 142 | /** 143 | * It MUST NOT be considered an error condition if the specified key does 144 | * not exist. The post-condition is the same (the key does not exist, or 145 | * the pool is empty), thus there is no error condition. 146 | */ 147 | public function testDeleteNonExistant() 148 | { 149 | $this->assertTrue($this->pool->deleteItem('non-existant')); 150 | $this->assertFalse($this->pool->hasItem('non-existant')); 151 | } 152 | 153 | public function testSaveDeferredAndCommit() 154 | { 155 | $item = $this->pool->getItem('foo')->set('foo value'); 156 | $this->assertSame($this->pool, $this->pool->saveDeferred($item)); 157 | 158 | $this->assertNull($item->get()); 159 | 160 | // get the deferred version 161 | $this->assertEquals('foo value', $this->pool->getItem('foo')); 162 | 163 | $this->assertTrue($this->pool->commit()); 164 | $this->assertEquals('foo value', $item); 165 | 166 | $items = $this->pool->getItems(array('foo', 'bar')); 167 | $this->assertEquals('foo value', $items['foo']); 168 | } 169 | 170 | /** 171 | * Regression test for bug GH#13 172 | * 173 | * @link https://github.com/frqnck/apix-cache/issues/13 174 | * "TaggablePool and Pool overrides prefix_key and prefix_tag options 175 | * with hardcoded values" 176 | * @group regression 177 | */ 178 | public function testBug13() 179 | { 180 | $pool = new Pool( 181 | new Cache\Runtime(array(), $this->options) 182 | ); 183 | $adapter = $pool->getCacheAdapter(); 184 | 185 | $this->assertSame( 186 | $this->options['prefix_key'], 187 | $adapter->getOption('prefix_key') 188 | ); 189 | $this->assertSame( 190 | $this->options['prefix_tag'], 191 | $adapter->getOption('prefix_tag') 192 | ); 193 | } 194 | 195 | public function testDestructDoesCommit() 196 | { 197 | $item = $this->pool->getItem('foo')->set('foo value'); 198 | $this->assertSame($this->pool, $this->pool->saveDeferred($item)); 199 | $this->pool->__destruct(); 200 | $this->assertEquals('foo value', $item); 201 | 202 | $item = $this->pool->getItem('foo'); 203 | $this->assertEquals('foo value', $item ); 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /tests/PsrCache/TaggableItemTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\PsrCache; 14 | 15 | use Apix\Cache\PsrCache; 16 | 17 | class TaggableItemTest extends ItemTest 18 | { 19 | protected $item = null; 20 | 21 | public function setUp() 22 | { 23 | $this->item = new PsrCache\TaggableItem('foo'); 24 | } 25 | 26 | public function tearDown() 27 | { 28 | unset($this->item); 29 | } 30 | 31 | public function testGetItemTagsIsNullByDefault() 32 | { 33 | $this->assertNull($this->item->getTags()); 34 | } 35 | 36 | public function testSetItemTags() 37 | { 38 | $tags = array('fooTag', 'barTag'); 39 | $this->assertSame($this->item, $this->item->setTags($tags)); 40 | $this->assertSame($tags, $this->item->getTags()); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/PsrCache/TaggablePoolTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\PsrCache; 14 | 15 | use Apix\Cache, 16 | Apix\Cache\PsrCache\TaggablePool; 17 | 18 | class TaggablePoolTest extends PoolTest 19 | { 20 | protected $cache = null, $pool = null, $item = null; 21 | 22 | public function setUp() 23 | { 24 | $this->cache = new Cache\Runtime(); 25 | 26 | $this->pool = new TaggablePool($this->cache); 27 | $this->item = $this->pool->getItem('foo')->set('foo value'); 28 | $this->pool->save($this->item); 29 | 30 | $this->assertTrue($this->item->isHit()); 31 | } 32 | 33 | public function tearDown() 34 | { 35 | if (null !== $this->cache) { 36 | $this->cache->flush(); 37 | unset($this->cache); 38 | } 39 | unset($this->pool, $this->item); 40 | } 41 | 42 | public function testGetItemsByTagIsEmptyArrayByDefault() 43 | { 44 | $this->assertEquals( 45 | array(), 46 | $this->pool->getItemsByTag('non-existant') 47 | ); 48 | } 49 | 50 | public function testGetItemsByTag() 51 | { 52 | $tags = array('fooTag', 'barTag'); 53 | $this->assertSame($this->item, $this->item->setTags($tags)); 54 | $this->assertTrue($this->pool->save($this->item)); 55 | 56 | $items = $this->pool->getItemsByTag('fooTag'); 57 | 58 | $this->assertInstanceOf('Apix\Cache\PsrCache\TaggableItem', $items['foo']); 59 | $this->assertSame('foo', $items['foo']->getkey()); 60 | $this->assertSame('foo value', $items['foo']->get()); 61 | 62 | $this->assertSame($tags, $items['foo']->getTags()); 63 | } 64 | 65 | public function testClearByTags() 66 | { 67 | $this->assertFalse($this->pool->clearByTags( array('non-existant') )); 68 | 69 | $tags = array('fooTag', 'barTag'); 70 | $this->assertSame($this->item, $this->item->setTags($tags)); 71 | $this->assertTrue($this->pool->save($this->item)); 72 | 73 | $this->assertTrue($this->pool->clearByTags( array('fooTag') )); 74 | } 75 | 76 | /** 77 | * Regression test for bug GH#13 78 | * 79 | * @link https://github.com/frqnck/apix-cache/issues/13 80 | * "TaggablePool and Pool overrides prefix_key and prefix_tag options 81 | * with hardcoded values" 82 | * @group regression 83 | */ 84 | public function testBug13() 85 | { 86 | $pool = new TaggablePool( 87 | new Cache\Runtime(array(), $this->options) 88 | ); 89 | $adapter = $pool->getCacheAdapter(); 90 | 91 | $this->assertSame( 92 | $this->options['prefix_key'], 93 | $adapter->getOption('prefix_key') 94 | ); 95 | $this->assertSame( 96 | $this->options['prefix_tag'], 97 | $adapter->getOption('prefix_tag') 98 | ); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tests/RedisTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | /** 18 | * RedisTest 19 | * 20 | * @package Apix\Cache 21 | * @author Franck Cassedanne 22 | */ 23 | class RedisTest extends GenericTestCase 24 | { 25 | const HOST = '127.0.0.1'; 26 | const PORT = 6379; 27 | const AUTH = null; 28 | 29 | protected $cache, $redis; 30 | 31 | public function setUp() 32 | { 33 | $this->skipIfMissing('redis'); 34 | 35 | try { 36 | $this->redis = new \Redis(); 37 | $this->redis->connect(self::HOST, self::PORT); 38 | if (self::AUTH) { 39 | $this->redis->auth(self::AUTH); 40 | } 41 | $this->redis->ping(); 42 | } catch (\Exception $e) { 43 | $this->markTestSkipped( $e->getMessage() ); 44 | } 45 | 46 | $this->cache = new Cache\Redis($this->redis, $this->options); 47 | } 48 | 49 | public function tearDown() 50 | { 51 | if (null !== $this->cache) { 52 | $this->cache->flush(); 53 | $this->redis->close(); 54 | unset($this->cache, $this->redis); 55 | } 56 | } 57 | 58 | public function testFlushSelected() 59 | { 60 | $this->assertTrue( 61 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 62 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 63 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 64 | ); 65 | 66 | $this->redis->set('foo', 'bar'); 67 | $this->assertTrue($this->cache->flush()); 68 | $this->assertFalse($this->cache->flush()); 69 | $this->assertTrue((boolean) $this->redis->exists('foo')); 70 | 71 | $this->assertNull($this->cache->loadKey('id3')); 72 | $this->assertNull($this->cache->loadTag('tag1')); 73 | } 74 | 75 | public function testFlushAll() 76 | { 77 | $this->assertTrue( 78 | $this->cache->save('data1', 'id1', array('tag1', 'tag2')) 79 | && $this->cache->save('data2', 'id2', array('tag2', 'tag3')) 80 | && $this->cache->save('data3', 'id3', array('tag3', 'tag4')) 81 | ); 82 | 83 | $this->redis->set('foo', 'bar'); 84 | $this->assertTrue($this->cache->flush(true)); // always true! 85 | $this->assertFalse((boolean) $this->redis->exists('foo')); 86 | 87 | $this->assertNull($this->cache->loadKey('id3')); 88 | $this->assertNull($this->cache->loadTag('tag1')); 89 | } 90 | 91 | public function testShortTtlDoesExpunge() 92 | { 93 | $this->cache->save('ttl-1', 'ttlId', null, -1); 94 | $this->assertNull( $this->cache->load('ttlId')); 95 | } 96 | 97 | public function testSetSerializerToPhp() 98 | { 99 | $this->cache->setSerializer('php'); 100 | $this->assertSame( 101 | \Redis::SERIALIZER_PHP, $this->cache->getSerializer() 102 | ); 103 | } 104 | 105 | public function testSetSerializerToIgBinary() 106 | { 107 | if (defined('Redis::SERIALIZER_IGBINARY')) { 108 | $this->cache->setSerializer('igBinary'); 109 | $this->assertSame( 110 | \Redis::SERIALIZER_IGBINARY, $this->cache->getSerializer() 111 | ); 112 | } 113 | } 114 | 115 | // msgpack 2.0.1, compatible with PHP 7 116 | public function testSetSerializerToMsgpack() 117 | { 118 | if (defined('Redis::SERIALIZER_MSGPACK')) { 119 | $this->cache->setSerializer('msgpack'); 120 | $this->assertSame( 121 | \Redis::SERIALIZER_MSGPACK, $this->cache->getSerializer() 122 | ); 123 | } 124 | } 125 | 126 | public function testSetSerializerToNull() 127 | { 128 | $this->cache->setSerializer(null); 129 | $this->assertSame( 130 | \Redis::SERIALIZER_NONE, $this->cache->getSerializer() 131 | ); 132 | } 133 | 134 | public function testSetSerializerToJson() 135 | { 136 | $this->cache->setSerializer('json'); 137 | $this->assertInstanceOf( 138 | 'Apix\Cache\Serializer\Json', $this->cache->getSerializer() 139 | ); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /tests/RuntimeTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | use Apix\Cache; 16 | 17 | /** 18 | * RuntimeTest 19 | * 20 | * @package Apix\Cache 21 | * @author Franck Cassedanne 22 | */ 23 | class RuntimeTest extends GenericTestCase 24 | { 25 | protected $cache = null; 26 | 27 | public function setUp() 28 | { 29 | $this->cache = new Cache\Runtime(null, $this->options); 30 | } 31 | 32 | public function tearDown() 33 | { 34 | if (null !== $this->cache) { 35 | $this->cache->flush(); 36 | unset($this->cache); 37 | } 38 | } 39 | 40 | public function testWithAPopulatedArray() 41 | { 42 | $options = array('prefix_key' => null, 'prefix_tag' => null); 43 | $pre_cached_items = array( 44 | 'foo' => array( 45 | 'data' => 'foo value', 'tags' => array('tag'), 'expire' => null 46 | ) 47 | ); 48 | 49 | $this->cache = new Cache\Runtime($pre_cached_items, $options); 50 | $this->assertSame('foo value', $this->cache->loadKey('foo')); 51 | $this->assertSame(array('foo'), $this->cache->loadTag('tag')); 52 | } 53 | 54 | // AbstratcCache 55 | 56 | public function testGetOption() 57 | { 58 | $this->assertSame($this->options['prefix_key'], $this->cache->getOption('prefix_key') ); 59 | } 60 | 61 | public function testRemovePrefix() 62 | { 63 | $this->assertSame( 64 | '-str', $this->cache->removePrefix('prefix-str', 'prefix') 65 | ); 66 | } 67 | 68 | public function testRemovePrefixKey() 69 | { 70 | $this->assertSame( 71 | 'foo', 72 | $this->cache->removePrefixKey($this->options['prefix_key'] . 'foo') 73 | ); 74 | $this->assertSame( 75 | 'not-prefixed-key', 76 | $this->cache->removePrefixKey('not-prefixed-key') 77 | ); 78 | } 79 | public function testRemovePrefixTag() 80 | { 81 | $this->assertSame( 82 | 'foo', 83 | $this->cache->removePrefixTag($this->options['prefix_tag'] . 'foo') 84 | ); 85 | $this->assertSame( 86 | 'not-prefixed-key', 87 | $this->cache->removePrefixTag('not-prefixed-key') 88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /tests/Serializer/IgbinaryTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\Igbinary; 16 | 17 | class IgbinaryTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->skipIfMissing('igbinary'); 23 | $this->serializer = new Igbinary; 24 | 25 | } 26 | 27 | /** 28 | * @dataProvider serializerProvider 29 | */ 30 | public function testSerialize($var) 31 | { 32 | $this->assertEquals( 33 | \igbinary_serialize($var), 34 | $this->serializer->serialize($var) 35 | ); 36 | } 37 | 38 | /** 39 | * @dataProvider serializerProvider 40 | */ 41 | public function testUnserialize($var) 42 | { 43 | $this->assertEquals( 44 | $var, 45 | $this->serializer->unserialize( 46 | \igbinary_serialize($var) 47 | ) 48 | ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Serializer/JsonTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\Json; 16 | 17 | class JsonTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->skipIfMissing('json'); 23 | $this->serializer = new Json; 24 | } 25 | 26 | /** 27 | * @dataProvider serializerProvider 28 | */ 29 | public function testSerialize($var) 30 | { 31 | $this->assertEquals( 32 | \json_encode($var), 33 | $this->serializer->serialize($var) 34 | ); 35 | } 36 | 37 | /** 38 | * @dataProvider serializerProvider 39 | */ 40 | public function testUnserialize($var) 41 | { 42 | if(is_array($var)) $var = (object) $var; 43 | 44 | $this->assertEquals( 45 | $var, 46 | $this->serializer->unserialize( 47 | \json_encode($var) 48 | ) 49 | ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /tests/Serializer/MsgpackTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\Msgpack; 16 | 17 | class MsgpackTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->skipIfMissing('msgpack'); 23 | $this->serializer = new Msgpack; 24 | } 25 | 26 | /** 27 | * @dataProvider serializerProvider 28 | */ 29 | public function testSerialize($var) 30 | { 31 | $this->assertEquals( 32 | \msgpack_pack($var), 33 | $this->serializer->serialize($var) 34 | ); 35 | } 36 | 37 | /** 38 | * @dataProvider serializerProvider 39 | */ 40 | public function testUnserialize($var) 41 | { 42 | $this->assertEquals( 43 | $var, 44 | $this->serializer->unserialize( 45 | \msgpack_pack($var) 46 | ) 47 | ); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /tests/Serializer/NoneTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\None; 16 | 17 | class NoneTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->serializer = new None; 23 | } 24 | 25 | /** 26 | * @dataProvider serializerProvider 27 | */ 28 | public function testSerialize($var) 29 | { 30 | $this->assertEquals( 31 | $var, 32 | $this->serializer->serialize($var) 33 | ); 34 | } 35 | 36 | /** 37 | * @dataProvider serializerProvider 38 | */ 39 | public function testUnserialize($var) 40 | { 41 | $this->assertEquals( 42 | $var, 43 | $this->serializer->unserialize($var) 44 | ); 45 | } 46 | 47 | /** 48 | * @dataProvider serializerProvider 49 | * @deprecated 50 | */ 51 | public function testIsSerialized($var, $str = null) 52 | { 53 | $this->assertFalse( 54 | $this->serializer->isSerialized($var) 55 | ); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/Serializer/PhpTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\Php; 16 | 17 | class PhpTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->serializer = new Php; 23 | } 24 | 25 | /** 26 | * @dataProvider serializerProvider 27 | */ 28 | public function testSerialize($var) 29 | { 30 | $this->assertEquals( 31 | \serialize($var), 32 | $this->serializer->serialize($var) 33 | ); 34 | } 35 | 36 | /** 37 | * @dataProvider serializerProvider 38 | */ 39 | public function testUnserialize($var) 40 | { 41 | $this->assertEquals( 42 | $var, 43 | $this->serializer->unserialize( 44 | \serialize($var) 45 | ) 46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/Serializer/StringsetTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\Serializer\Stringset; 16 | 17 | class StringsetTest extends TestCase 18 | { 19 | 20 | public function setUp() 21 | { 22 | $this->serializer = new Stringset(); 23 | } 24 | 25 | public function serializerProvider() 26 | { 27 | return array( 28 | array(array('a', 'b', 'c'), 'a b c '), 29 | array(array('juju', 'molly', 'loulou'), 'juju molly loulou '), 30 | array(array('a-b', 'c', 'd'), 'a-b c d '), 31 | array(array(), ''), 32 | array(null, '') 33 | ); 34 | } 35 | 36 | /** 37 | * @dataProvider serializerProvider 38 | */ 39 | public function testSerialize($var, $exp) 40 | { 41 | $this->assertEquals( 42 | $exp, 43 | $this->serializer->serialize($var) 44 | ); 45 | } 46 | 47 | public function unserializerProvider() 48 | { 49 | return array( 50 | array(array('a', 'b', 'c'), 'a b c ', 0), 51 | array(array('a', 'c'), 'a b c -b -x ', 2), 52 | array(array('c'), 'a b c -b -x -a', 3), 53 | array(null, 'a -a', 1) 54 | ); 55 | } 56 | 57 | /** 58 | * @dataProvider unserializerProvider 59 | */ 60 | public function testUnserializer($arr, $str, $dirt) 61 | { 62 | $this->assertEquals( 63 | $arr, 64 | $this->serializer->unserialize($str) 65 | ); 66 | $this->assertEquals( 67 | $dirt, 68 | $this->serializer->getDirtiness() 69 | ); 70 | } 71 | 72 | public function isSerializerProvider() 73 | { 74 | return array( 75 | array(array('a', 'b', 'c'), 'a b c '), 76 | array(array('juju', 'molly', 'loulou'), 'juju molly loulou '), 77 | array(array('a-b', 'c', 'd'), 'a-b c d ') 78 | ); 79 | } 80 | 81 | /** 82 | * @dataProvider isSerializerProvider 83 | * @deprecated 84 | */ 85 | public function testIsSerialized($var, $str = null) 86 | { 87 | $this->assertFalse( 88 | $this->serializer->isSerialized($var) 89 | ); 90 | $this->assertTrue( 91 | $this->serializer->isSerialized($str) 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Serializer/TestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests\Serializer; 14 | 15 | use Apix\Cache\tests\TestCase as ApixTestCase; 16 | 17 | class TestCase extends ApixTestCase 18 | { 19 | protected $serializer = null; 20 | 21 | public function serializerProvider() 22 | { 23 | return array( 24 | array('string'), 25 | array(array('foo' => 'bar')), 26 | array(new \stdClass()), 27 | array(' '), 28 | // array(null), 29 | ); 30 | } 31 | 32 | /** 33 | * @dataProvider serializerProvider 34 | * @deprecated 35 | */ 36 | public function testIsSerialized($var, $str = null) 37 | { 38 | $this->assertFalse( 39 | $this->serializer->isSerialized($var) 40 | ); 41 | 42 | $this->assertTrue( 43 | $this->serializer->isSerialized( 44 | $this->serializer->serialize($var) 45 | ) 46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 10 | * 11 | */ 12 | 13 | namespace Apix\Cache\tests; 14 | 15 | /** 16 | * Generic TestCase 17 | * 18 | * @package Apix\Cache 19 | * @author Franck Cassedanne 20 | */ 21 | class TestCase extends \PHPUnit_Framework_TestCase 22 | { 23 | 24 | protected $options = array( 25 | 'prefix_key' => 'unittest-apix-key:', 26 | 'prefix_tag' => 'unittest-apix-tag:' 27 | ); 28 | 29 | public function skipIfMissing($name) 30 | { 31 | if (defined('HHVM_VERSION')) { 32 | switch($name) { 33 | case 'redis': 34 | #case 'mongo': 35 | self::markTestSkipped( 36 | sprintf('`%s` cannot be used with HHVM.', $name) 37 | ); 38 | return; 39 | 40 | default: 41 | } 42 | } 43 | 44 | if (!extension_loaded($name)) { 45 | $prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : ''; 46 | if ( 47 | !ini_get('enable_dl') 48 | || !dl($prefix . "$name." . PHP_SHLIB_SUFFIX)) { 49 | self::markTestSkipped( 50 | sprintf('The `%s` extension is required.', $name) 51 | ); 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/bin/travis-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | VERSION=`phpenv version-name` 6 | 7 | function pecl_install() 8 | { 9 | local _PKG=$1 10 | local _VERSION=$2 11 | 12 | pecl shell-test $_PKG || (echo "yes" | pecl install $_PKG-$_VERSION) 13 | } 14 | 15 | if [ "${VERSION}" = "hhvm" ]; then 16 | PHPINI=/etc/hhvm/php.ini 17 | else 18 | PHPINI=~/.phpenv/versions/$VERSION/etc/php.ini 19 | 20 | # update PECL 21 | pecl channel-update pecl.php.net 22 | 23 | # install igbinary 24 | pecl_install igbinary 2.0.8 25 | 26 | # install msgpack 27 | if [ "$(expr "${VERSION}" "<" "7.0")" -eq 1 ] 28 | then 29 | pecl_install msgpack 0.5.7 30 | else 31 | pecl_install msgpack 2.0.3 32 | fi 33 | fi 34 | 35 | # enable xdebug 36 | if [[ $TRAVIS_PHP_VERSION =~ ^hhvm ]] 37 | then 38 | echo 'xdebug.enable = On' >> $PHPINI 39 | fi 40 | 41 | # 42 | # APC 43 | # 44 | if [ "$DB" = "apc" ]; then 45 | if [ "${VERSION}" = "hhvm" ] || [ "$(expr "${VERSION}" "<" "5.5")" -eq 1 ] 46 | then 47 | echo "extension = apc.so" >> $PHPINI 48 | elif [ "$(expr "${VERSION}" "<" "7.0")" -eq 1 ] 49 | then 50 | pecl_install apcu 4.0.11 51 | else 52 | pecl_install apcu 5.1.8 53 | fi 54 | echo "apc.enable_cli = 1" >> $PHPINI 55 | fi 56 | 57 | # 58 | # Redis 59 | # 60 | if [ "$DB" = "redis" ]; then 61 | git clone --branch=master --depth=1 git://github.com/nicolasff/phpredis.git phpredis 62 | cd phpredis && phpize && ./configure && make && sudo make install && cd .. 63 | rm -fr phpredis 64 | echo "extension = redis.so" >> $PHPINI 65 | fi 66 | 67 | # 68 | # Mongo DB 69 | # 70 | if [ "$DB" = "mongodb" ]; then 71 | if [ "${VERSION}" != "hhvm" ] && [ "$(expr "${VERSION}" "<" "7.0")" -eq 1 ] 72 | then 73 | echo "extension = mongo.so" >> $PHPINI 74 | else 75 | if [ "${VERSION}" = "hhvm" ] 76 | then 77 | # Instal HHVM 78 | sudo apt-get install -y cmake 79 | git clone git://github.com/facebook/hhvm.git 80 | cd hhvm && git checkout 1da451b && cd - # Tag:3.0.1 81 | export HPHP_HOME=`pwd`/hhvm 82 | 83 | # Install mongo-hhvm-driver 84 | git clone https://github.com/mongodb/mongo-hhvm-driver.git --branch 1.1.3 85 | cd mongo-hhvm-driver 86 | git submodule sync && git submodule update --init --recursive 87 | $HPHP_HOME/hphp/tools/hphpize/hphpize 88 | cmake . && make configlib && make -j 2 && make install 89 | echo "hhvm.dynamic_extensions[mongodb] = mongodb.so" >> $PHPINI 90 | fi 91 | echo "extension = mongodb.so" >> $PHPINI 92 | composer require "mongodb/mongodb=^1.0.0" 93 | fi 94 | fi 95 | 96 | # 97 | # MySQL 98 | # 99 | if [ "$DB" = "mysql" ]; then 100 | mysql -e 'CREATE DATABASE IF NOT EXISTS apix_tests;' 101 | fi 102 | 103 | # 104 | # Postgres 105 | # 106 | if [ "$DB" = "pgsql" ]; then 107 | psql -c 'DROP DATABASE IF EXISTS apix_tests;' -U postgres 108 | psql -c 'CREATE DATABASE apix_tests;' -U postgres 109 | fi 110 | 111 | # 112 | # Memcached 113 | # 114 | if [ "$DB" = "memcached" ]; then 115 | echo "extension = memcached.so" >> $PHPINI 116 | fi 117 | --------------------------------------------------------------------------------