diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..90ae313 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-ci +coverage_clover: build/logs/clover.xml diff --git a/.formatter.yml b/.formatter.yml index 5860833..3225af2 100644 --- a/.formatter.yml +++ b/.formatter.yml @@ -15,4 +15,4 @@ header: | * file that was distributed with this source code. * * @author Daniel González - */ \ No newline at end of file + */ diff --git a/.gitignore b/.gitignore index d2e3f0a..2a66a32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ /build /composer.lock -/tests/config.yml +/tests/config.json /vendor /.idea +/TODO.md *~ *.swp diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 2e881b5..3788948 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -81,4 +81,4 @@ tools: - 'vendor/*' # Security Advisory Checker - sensiolabs_security_checker: true \ No newline at end of file + sensiolabs_security_checker: true diff --git a/.travis.yml b/.travis.yml index 4b91d35..f052dff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,45 @@ language: php +#sudo: false php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - hhvm + - 7.1 + - 7.2 + - nightly + +matrix: + allow_failures: + - php: nightly services: - - mongodb - - redis-server - - memcached + - mysql + - mongodb + - redis-server + - memcached notifications: - email: - - daniel.gonzalez@freelancemadrid.es + email: + - daniel.gonzalez@freelancemadrid.es before_script: + # Create Mysql Database + - mysql -e 'CREATE DATABASE IF NOT EXISTS `cache`;' + - mysql -e 'USE `cache`; CREATE TABLE `cache` (`k` varchar(255) NOT NULL, `v` text NOT NULL, `t` int(11) NOT NULL, PRIMARY KEY (`k`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;' + + # Enable extensions + - yes '' | pecl install --soft mongodb + - ls .travis/*.ini | xargs phpenv config-add + - test "$TRAVIS_PHP_VERSION" != "nightly" || phpenv config-rm memcached.ini || true + + # Install dependencies + - composer self-update + - composer install --ignore-platform-reqs + - composer require --dev "satooshi/php-coveralls:~0.6" --ignore-platform-reqs + - mkdir -p build/logs -# Create MySQL Database - - mysql -e 'CREATE DATABASE IF NOT EXISTS `cache`;' - - mysql -e 'USE `cache`; CREATE TABLE `cache` (`hash` varchar(255) NOT NULL, `value` text NOT NULL, `ttl` int(11) NOT NULL, PRIMARY KEY (`hash`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;' +script: + - phpunit -v --coverage-text --coverage-xml=./build/logs/clover.xml -# Install dependencies - - composer install --prefer-source +after_script: + - ls + - php vendor/bin/coveralls -v -# Set Configuration - - cp tests/config.yml.dist tests/config.yml \ No newline at end of file diff --git a/.travis/apcu.ini b/.travis/apcu.ini new file mode 100644 index 0000000..782fd5f --- /dev/null +++ b/.travis/apcu.ini @@ -0,0 +1,6 @@ +extension="apcu.so" +extension="memcached.so" +extension="redis.so" + +apc.enabled=1 +apc.enable_cli=1 diff --git a/.travis/memcached.ini b/.travis/memcached.ini new file mode 100644 index 0000000..6b9b24f --- /dev/null +++ b/.travis/memcached.ini @@ -0,0 +1 @@ +extension="memcached.so" diff --git a/.travis/mongodb.ini b/.travis/mongodb.ini new file mode 100644 index 0000000..0d77e9f --- /dev/null +++ b/.travis/mongodb.ini @@ -0,0 +1 @@ +extension="mongodb.so" diff --git a/.travis/redis.ini b/.travis/redis.ini new file mode 100644 index 0000000..61c6d5c --- /dev/null +++ b/.travis/redis.ini @@ -0,0 +1 @@ +extension="redis.so" diff --git a/LICENSE b/LICENSE index 5bc389e..2dd6c18 100644 --- a/LICENSE +++ b/LICENSE @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/README.md b/README.md index 21d162e..d6203cb 100644 --- a/README.md +++ b/README.md @@ -1,191 +1,172 @@ -# Cache +# Desarolla2 Cache -A simple cache library. Implements different adapters that you can use and change -easily by a manager or similar. +A **simple cache** library, implementing the [PSR-16](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) standard using **immutable** objects. -[![Build Status](https://secure.travis-ci.org/desarrolla2/Cache.png)](http://travis-ci.org/desarrolla2/Cache) [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/desarrolla2/Cache/badges/quality-score.png?s=940939c8d0bf2056188455594f4332a002a968c2)](https://scrutinizer-ci.com/g/desarrolla2/Cache/) [![Code Coverage](https://scrutinizer-ci.com/g/desarrolla2/Cache/badges/coverage.png?s=16037142f461dcfdfd6ad57561e231881252197b)](https://scrutinizer-ci.com/g/desarrolla2/Cache/) +![life-is-hard-cache-is](https://user-images.githubusercontent.com/100821/41566888-ecd60cde-735d-11e8-893f-da42b2cd65e7.jpg) -[![Latest Stable Version](https://poser.pugx.org/desarrolla2/cache/v/stable.png)](https://packagist.org/packages/desarrolla2/cache) [![Total Downloads](https://poser.pugx.org/desarrolla2/cache/downloads.png)](https://packagist.org/packages/desarrolla2/cache) +Caching is typically used throughout an applicatiton. Immutability ensure that modifying the cache behaviour in one +location doesn't result in unexpected behaviour due to changes in unrelated code. +_Desarolla2 Cache aims to be the most complete, correct and best performing PSR-16 implementation available._ +[![Latest version][ico-version]][link-packagist] +[![Latest version][ico-pre-release]][link-packagist] +[![Software License][ico-license]][link-license] +[![Build Status][ico-travis]][link-travis] +[![Coverage Status][ico-coveralls]][link-coveralls] +[![Quality Score][ico-code-quality]][link-code-quality] +[![Sensiolabs Insight][ico-sensiolabs]][link-sensiolabs] +[![Total Downloads][ico-downloads]][link-downloads] +[![Today Downloads][ico-today-downloads]][link-downloads] +[![Gitter][ico-gitter]][link-gitter] -## Installation - -### With Composer -It is best installed it through [packagist](http://packagist.org/packages/desarrolla2/cache) -by including `desarrolla2/cache` in your project composer.json require: +## Installation -``` json - "require": { - // ... - "desarrolla2/cache": "dev-master" - } ``` - -### Without Composer - -You can also download it from [Github] (https://github.com/desarrolla2/Cache), -but no autoloader is provided so you'll need to register it with your own PSR-0 -compatible autoloader. +composer require desarrolla2/cache +``` ## Usage ``` php -set('key', 'myKeyValue', 3600); +$cache = new Cache(); -// later ... +$value = $cache->get('key'); -echo $cache->get('key'); +if (!isset($value)) { + $value = do_something(); + $cache->set('key', $value, 3600); +} +echo $value; ``` ## Adapters -### NotCache - -Use it if you will not implement any cache adapter is an adapter that will serve -to fool the test environments. - -### File - -Use it if you will you have dont have other cache system available in your system -or if you like to do your code more portable. - -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); - -``` - -### Apc - -Use it if you will you have APC cache available in your system. - -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); +* [Apcu](docs/implementations/apcu.md) +* [File](docs/implementations/file.md) +* [Memcached](docs/implementations/memcached.md) +* [Memory](docs/implementations/memory.md) +* [MongoDB](docs/implementations/mongodb.md) +* [Mysqli](docs/implementations/mysqli.md) +* [NotCache](docs/implementations/notcache.md) +* [PhpFile](docs/implementations/phpfile.md) +* [Predis](docs/implementations/predis.md) -``` +The following implementation allows you to combine cache adapters. -### Memory +* [Chain](docs/implementations/chain.md) -This is the fastest cache type, since the elements are stored in memory. -Cache Memory such is very volatile and is removed when the process terminates. -Also it is not shared between different processes. +[Other implementations][todo-implementations] are planned. Please vote or +provide a PR to speed up the process of adding the to this library. -Memory cache have a option "limit", that limit the max items in cache. +[todo-implementations]: https://github.com/desarrolla2/Cache/issues?q=is%3Aissue+is%3Aopen+label%3Aadapter -``` php -setOption('ttl', 3600); -$adapter->setOption('limit', 200); -$cache = new Cache($adapter); +You can set options for cache using the `withOption` or `withOptions` method. +Note that all cache objects are immutable, setting an option creates a new +object. -``` +#### TTL -### Mongo +All cache implementations support the `ttl` option. This sets the default +time (in seconds) that cache will survive. It defaults to one hour (3600 +seconds). -Use it if you will you have mongodb available in your system. +Setting the TTL to 0 or a negative number, means the cache should live forever. -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); +Each cache implementation has the following `Psr\SimpleCache\CacheInterface` +methods: -``` +##### `get(string $key [, mixed $default])` +Retrieve the value corresponding to a provided key -### MySQL +##### `has(string $key)` +Retrieve the if value corresponding to a provided key exist -Use it if you will you have mysqlnd available in your system. +##### `set(string $key, mixed $value [, int $ttl])` +Add a value to the cache under a unique key -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); +##### `getMultiple(array $keys)` +Obtains multiple cache items by their unique keys -``` +##### `setMultiple(array $values [, int $ttl])` +Persists a set of key => value pairs in the cache -### Redis +##### `deleteMultiple(array $keys)` +Deletes multiple cache items in a single operation -Use it if you will you have redis available in your system. +. -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); +##### `withOptions(array $options)` +Set multiple options for implementation. Creates a new instance. -``` +##### `getOption(string $key)` +Get option for implementation. -### Memcache -Use it if you will you have memcache available in your system. +## Packers -``` php -setOption('ttl', 3600); -$cache = new Cache($adapter); +* `SerializePacker` using `serialize` and `unserialize` +* `JsonPacker` using `json_encode` and `json_decode` +* `NopPacker` does no packing +* `MongoDBBinaryPacker` using `serialize` and `unserialize` to store as [BSON Binary](http://php.net/manual/en/class.mongodb-bson-binary.php) -``` +#### PSR-16 incompatible packers -## Coming soon +The `JsonPacker` does not fully comply with PSR-16, as packing and +unpacking an object will probably not result in an object of the same class. -This library implements other adapters as soon as possible, feel free to send -new adapters if you think it appropriate. +The `NopPacker` is intended when caching string data only (like HTML output) or +if the caching backend supports structured data. Using it when storing objects +will might give unexpected results. -This can be a list of pending tasks. +## Contributors -* Cleaning cache -* MemcachedAdapter -* Other Adapters +[![Daniel González](https://avatars1.githubusercontent.com/u/661529?v=3&s=80)](https://github.com/desarrolla2) +Twitter: [@desarrolla2](https://twitter.com/desarrolla2)\ +[![Arnold Daniels](https://avatars3.githubusercontent.com/u/100821?v=3&s=80)](https://github.com/jasny) +Twitter: [@ArnoldDaniels](https://twitter.com/ArnoldDaniels) -## Contact +[ico-version]: https://img.shields.io/packagist/v/desarrolla2/Cache.svg?style=flat-square +[ico-pre-release]: https://img.shields.io/packagist/vpre/desarrolla2/Cache.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-travis]: https://img.shields.io/travis/desarrolla2/Cache/master.svg?style=flat-square +[ico-coveralls]: https://img.shields.io/coveralls/desarrolla2/Cache/master.svg?style=flat-square +[ico-code-quality]: https://img.shields.io/scrutinizer/g/desarrolla2/cache.svg?style=flat-square +[ico-sensiolabs]: https://img.shields.io/sensiolabs/i/5f139261-1ac1-4559-846a-723e09319a88.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/desarrolla2/cache.svg?style=flat-square +[ico-today-downloads]: https://img.shields.io/packagist/dd/desarrolla2/cache.svg?style=flat-square +[ico-gitter]: https://img.shields.io/badge/GITTER-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat-square -You can contact with me on [@desarrolla2](https://twitter.com/desarrolla2). \ No newline at end of file +[link-packagist]: https://packagist.org/packages/desarrolla2/cache +[link-license]: http://hassankhan.mit-license.org +[link-travis]: https://travis-ci.org/desarrolla2/Cache +[link-coveralls]: https://coveralls.io/github/desarrolla2/Cache +[link-code-quality]: https://scrutinizer-ci.com/g/desarrolla2/cache +[link-sensiolabs]: https://insight.sensiolabs.com/projects/5f139261-1ac1-4559-846a-723e09319a88 +[link-downloads]: https://packagist.org/packages/desarrolla2/cache +[link-gitter]: https://gitter.im/desarrolla2/Cache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge diff --git a/build.xml b/build.xml index 18ff20b..4a5ff01 100644 --- a/build.xml +++ b/build.xml @@ -83,4 +83,18 @@ - \ No newline at end of file + + + + + + + + + + + + + + + diff --git a/composer.json b/composer.json index 6ada70e..88731af 100644 --- a/composer.json +++ b/composer.json @@ -1,35 +1,58 @@ { "name": "desarrolla2/cache", - "description": "Provides an cache interface for several adapters (Apc, File, Mongo, Memcached, Mysql, ... )", - "keywords": ["cache", "apc", "file", "memcached", "mysql", "mongo", "redis"], + "description": "Provides an cache interface for several adapters Apc, Apcu, File, Mongo, Memcache, Memcached, Mysql, Mongo, Redis is supported.", + "keywords": [ + "cache", + "simple-cache", + "psr-16", + "apc", + "apcu", + "file", + "memcached", + "memcache", + "mysql", + "mongo", + "redis" + ], "type": "library", "license": "MIT", - "homepage": "https://github.com/desarrolla2/Cache/blob/master/README.md", + "homepage": "https://github.com/desarrolla2/Cache/", "authors": [ { "name": "Daniel González", - "homepage": "http://desarrolla2.com/", - "role": "Developer" + "homepage": "http://desarrolla2.com/" + }, + { + "name": "Arnold Daniels", + "homepage": "https://jasny.net/" } ], - "support": { - "issues": "https://github.com/desarrolla2/Cache/issues" + "provide": { + "psr/simple-cache-implementation": "1.0" + }, + "require": { + "php": ">=7.1.0", + "psr/simple-cache": "^1.0", + "webmozart/glob": "^4.1" + }, + "require-dev": { + "ext-apcu": "*", + "ext-mysqli": "*", + "ext-memcached": "*", + "predis/predis": "~1.0.0", + "mongodb/mongodb": "^1.3", + "cache/integration-tests": "dev-master", + "phpunit/phpunit": "^7.2", + "mikey179/vfsStream": "^1.6" }, - "require": - { - "php": ">=5.3.0" - }, - "require-dev": - { - "raulfraile/ladybug": "v0.7", - "symfony/yaml": "dev-master", - "predis/predis": "*" - }, - "autoload": - { - "psr-4": { - "Desarrolla2\\Cache\\": "src/" - } - }, - "minimum-stability": "dev" + "autoload": { + "psr-4": { + "Desarrolla2\\Cache\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Desarrolla2\\Test\\Cache\\": "tests/" + } + } } diff --git a/docs/implementations/apcu.md b/docs/implementations/apcu.md new file mode 100644 index 0000000..711f42d --- /dev/null +++ b/docs/implementations/apcu.md @@ -0,0 +1,25 @@ +# Apcu + +Use [APCu cache](http://php.net/manual/en/book.apcu.php) to cache to shared +memory. + +``` php +use Desarrolla2\Cache\Apcu as ApcuCache; + +$cache = new ApcuCache(); +``` + +_Note: by default APCu uses the time at the beginning of a request for ttl. In +some cases, like with a long running script, this can be a problem. You can +change this behaviour `ini_set('apc.use_request_time', false)`._ + +### Options + +| name | type | default | | +| --------- | ---- | ------- | ------------------------------------- | +| ttl | int | null | Maximum time to live in seconds | +| prefix | string | "" | Key prefix | + +### Packer + +By default the [`NopPacker`](../packers/nop.md) is used. diff --git a/docs/implementations/chain.md b/docs/implementations/chain.md new file mode 100644 index 0000000..4aa5c11 --- /dev/null +++ b/docs/implementations/chain.md @@ -0,0 +1,37 @@ +# Chain + +The Cache chain allows you to use multiple implementations to store cache. For +instance, you can use both fast volatile (in-memory) storage and slower +non-volatile (disk) storage. Alternatively you can have a local storage +as well as a shared storage service. + +``` php +use Desarrolla2\Cache\Chain as CacheChain; +use Desarrolla2\Cache\Memory as MemoryCache; +use Desarrolla2\Cache\Predis as PredisCache; + +$cache = new CacheChain([ + (new MemoryCache())->withOption('ttl', 3600), + (new PredisCache())->withOption('ttl', 10800) +]); +``` + +The Chain cache implementation doesn't use any option. It uses the `Nop` packer +by default. + +Typically it's useful to specify a maximum `ttl` for each implementation. This +means that the volatile memory only holds items that are used often. + +The following actions propogate to all cache adapters in the chain + +* `set` +* `setMultiple` +* `delete` +* `deleteMultiple` +* `clear` + +For the following actions all nodes are tried in sequence + +* `has` +* `get` +* `getMultiple` \ No newline at end of file diff --git a/docs/implementations/file.md b/docs/implementations/file.md new file mode 100644 index 0000000..1902f23 --- /dev/null +++ b/docs/implementations/file.md @@ -0,0 +1,82 @@ +# File + +Save the cache as file to on the filesystem. + +You must pass a cache directory to the constructor. + +``` php +use Desarrolla2\Cache\File as FileCache; + +$cache = new FileCache(sys_get_temp_dir() . '/cache'); +``` + +### Options + +| name | type | default | | +| ------------ | --------------------------------- | -------------- | ------------------------------------- | +| ttl | int | null | Maximum time to live in seconds | +| ttl-strategy | string ('embed', 'file', 'mtime') | "embed" | Strategy to store the TTL | +| prefix | string | "" | Key prefix | +| filename | string or callable | "%s.php.cache" | Filename as sprintf format | + +#### TTL strategy option + +The ttl strategy determines how the TTL is stored. Typical filesystems don't +allow custom file properties, so we'll have to use one of these strategies: + +| strategy | | +| -------- | ----------------------------------------------- | +| embed | Embed the TTL as first line of the file | +| file | Create a TTL file in addition to the cache file | +| mtime | Use [mtime][] + max ttl | + +The 'mtime' strategy is not PSR-16 compliant, as the TTL passed to the `set()` +method is ignored. Only the `ttl` option for is used on `get()` and `has()`. + +[mtime]: https://www.unixtutorial.org/2008/04/atime-ctime-mtime-in-unix-filesystems/ + +#### Filename option + +The `filename` will be parsed using `sprintf` where '%s' is substituted with +the key. The default extension is automatically determined based on the +packer. + +Instead of a string, `filename` may also be set to a callable, like a callable +object or closure. In that case the callable will be called to create a +filename as + + $filename = $callable($key); + +##### BasicFilename + +The library comes with invokable object as callable for the filename. The +`BasicFilename` object works as described above. + +##### TrieFilename + +The `TrieFilename` object will create a prefix tree directory structure. This +is useful where a lot of cache files would cause to many files in a directory. + +Specify the `sprintf` format and the directory level to the constructor when +creating a `TrieFilename` object. + +``` php +use Desarrolla2\Cache\File as FileCache; +use Desarrolla2\Cache\File\TrieFilename; + +$callback = new TrieFilename('%s.php.cache', 2); + +$cache = (new FileCache(sys_get_temp_dir() . '/cache')) + ->withOption('filename', $callback); +``` + +In this case, adding an item with key `foobar` would be create a file at + + /tmp/cache/f/fo/foobar.php.cache + +### Packer + +By default the [`SerializePacker`](../packers/serialize.md) is used. The +[`NopPacker`](../packers/nop.md) can be used if the values are strings. +Other packers, like the [`JsonPacker`](../packers/json.md) are also +useful with file cache. diff --git a/docs/implementations/memcached.md b/docs/implementations/memcached.md new file mode 100644 index 0000000..8e00eb0 --- /dev/null +++ b/docs/implementations/memcached.md @@ -0,0 +1,28 @@ +# Memcached + +Store cache to [Memcached](https://memcached.org/). Memcached is a high +performance distributed caching system. + +``` php +use Desarrolla2\Cache\Memcached as MemcachedCache; +use Memcached; + +$server = new Memcached(); +// configure it here + +$cache = new MemcachedCache($server); +``` + +This implementation uses the [memcached](https://php.net/memcached) php +extension. The (alternative) memcache extension is not supported. + +### Options + +| name | type | default | | +| --------- | ---- | ------- | ------------------------------------- | +| ttl | int | null | Maximum time to live in seconds | +| prefix | string | "" | Key prefix | + +### Packer + +By default the [`NopPacker`](../packers/nop.md) is used. diff --git a/docs/implementations/memory.md b/docs/implementations/memory.md new file mode 100644 index 0000000..03d1262 --- /dev/null +++ b/docs/implementations/memory.md @@ -0,0 +1,23 @@ +# Memory + +Store the cache in process memory _(in other words in an array)_. Cache Memory +is removed when the PHP process exist. Also it is not shared between different +processes. + +``` php +use Desarrolla2\Cache\Memory as MemoryCache; + +$cache = new MemoryCache(); +``` + +### Options + +| name | type | default | | +| --------- | ---- | ------- | ------------------------------------- | +| ttl | int | null | Maximum time to live in seconds | +| limit | int | null | Maximum items in cache | +| prefix | string | "" | Key prefix | + +### Packer + +By default the [`SerializePacker`](../packers/serialize.md) is used. diff --git a/docs/implementations/mongodb.md b/docs/implementations/mongodb.md new file mode 100644 index 0000000..3bad03d --- /dev/null +++ b/docs/implementations/mongodb.md @@ -0,0 +1,45 @@ +# Mongo + +Use it to store the cache in a Mongo database. Requires the mongodb extension +and the [mongodb/mongodb](https://github.com/mongodb/mongo-php-library) +library. + +You must pass a `MongoDB\Collection` object to the cache constructor. + +``` php +selectDatabase('mycache'); +$collection = $database->selectCollection('cache'); + +$cache = new MongoCache($collection); +``` + +MonoDB will always automatically create the database and collection if needed. + +### Options + +| name | type | default | | +| --------- | ---- | ------- | ------------------------------------- | +| initialize | bool | true | Enable auto-initialize | +| ttl | int | null | Maximum time to live in seconds | +| prefix | string | "" | Key prefix | + +#### Initialize option + +If `initialize` is enabled, the cache implementation will automatically create +a [ttl index](https://docs.mongodb.com/manual/core/index-ttl/). In production +it's better to disable auto-initialization and create the ttl index explicitly +when setting up the database. This prevents a `createIndex()` call on each +request. + +### Packer + +By default the [`MongoDBBinaryPacker`](../packers/mongodbbinary.md) is used. It +serializes the data and stores it in a [Binary BSON variable](http://php.net/manual/en/class.mongodb-bson-binary.php). +If the data is a UTF-8 string of simple array or stdClass object, it may be +useful to use the [`NopPacker`](../packers/nop.md) instead. diff --git a/docs/implementations/mysqli.md b/docs/implementations/mysqli.md new file mode 100644 index 0000000..df676e3 --- /dev/null +++ b/docs/implementations/mysqli.md @@ -0,0 +1,47 @@ +# Mysqli + +Cache to a [MySQL database](https://www.mysql.com/) using the +[mysqli](http://php.net/manual/en/book.mysqli.php) PHP extension. + +You must pass a `mysqli` connection object to the constructor. + +``` php +withOption('filename', $callback); +``` + +In this case, adding an item with key `foobar` would be create a file at + + /tmp/cache/f/fo/foobar.php + +### Packer + +By default the [`NopPacker`](../packers/nop.md) is used. Other packers should +not be used. + +[read more]: https://medium.com/@dylanwenzlau/500x-faster-caching-than-redis-memcache-apc-in-php-hhvm-dcd26e8447ad diff --git a/docs/implementations/predis.md b/docs/implementations/predis.md new file mode 100644 index 0000000..d29fa96 --- /dev/null +++ b/docs/implementations/predis.md @@ -0,0 +1,31 @@ +# Predis + +Cache using a [redis server](https://redis.io/). Redis is an open source, +in-memory data structure store, used as a database, cache and message broker. + +You must provide a `Predis\Client` object to the constructor. + +```php +use Desarrolla2\Cache\Predis as PredisCache; +use Predis\Client as PredisClient; + +$client = new PredisClient('tcp://localhost:6379'); +$cache = new PredisCache($client); +``` + +### Installation + +Requires the [`predis`](https://github.com/nrk/predis/wiki) library. + + composer require predis/predis + +### Options + +| name | type | default | | +| --------- | ---- | ------- | ------------------------------------- | +| ttl | int | null | Maximum time to live in seconds | +| prefix | string | "" | Key prefix | + +### Packer + +By default the [`SerializePacker`](../packers/serialize.md) is used. diff --git a/doc/performance.md b/docs/performance.md similarity index 93% rename from doc/performance.md rename to docs/performance.md index 5e79728..4b01b2e 100644 --- a/doc/performance.md +++ b/docs/performance.md @@ -5,7 +5,7 @@ Here are my performance tests, you can view the results ordered from faster to s | Adapter | 10.000 set | 10.000 has | 10.000 get | | :-------------- | -----------: | -----------: | ---------: | | NoCache | 0.0637 | 0.0482 | 0.0488 | -| Apc | 0.0961 | 0.0556 | 0.0770 | +| Apcu | 0.0961 | 0.0556 | 0.0770 | | File | 0.6881 | 0.3426 | 0.3107 | | Mongo | 13.8144 | 30.0203 | 24.4214 | diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index a253f39..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - ./tests - - - - - - ./Test - - - /usr/share/pear/ - ./vendor - ./build - - - - - - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c49dbde --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,39 @@ + + + + + + ./tests + + + + + + ./src + + + + + + + + + + + + + + + + + diff --git a/src/AbstractCache.php b/src/AbstractCache.php new file mode 100644 index 0000000..65179ee --- /dev/null +++ b/src/AbstractCache.php @@ -0,0 +1,299 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\CacheInterface; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Option\PrefixTrait as PrefixOption; +use Desarrolla2\Cache\Option\TtlTrait as TtlOption; +use Desarrolla2\Cache\Packer\PackingTrait as Packing; +use Desarrolla2\Cache\Exception\CacheException; +use Desarrolla2\Cache\Exception\InvalidArgumentException; +use Desarrolla2\Cache\Exception\CacheExpiredException; +use DateTimeImmutable; +use DateInterval; + +/** + * AbstractAdapter + */ +abstract class AbstractCache implements CacheInterface +{ + use PrefixOption; + use TtlOption; + use Packing; + + /** + * Make a clone of this object. + * + * @return static + */ + protected function cloneSelf(): self + { + return clone $this; + } + + /** + * {@inheritdoc} + */ + public function withOption(string $key, $value): self + { + return $this->withOptions([$key => $value]); + } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $cache = $this->cloneSelf(); + + foreach ($options as $key => $value) { + $method = "set" . str_replace('-', '', $key) . "Option"; + + if (empty($key) || !method_exists($cache, $method)) { + throw new InvalidArgumentException("unknown option '$key'"); + } + + $cache->$method($value); + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function getOption($key) + { + $method = "get" . str_replace('-', '', $key) . "Option"; + + if (empty($key) || !method_exists($this, $method)) { + throw new InvalidArgumentException("unknown option '$key'"); + } + + return $this->$method(); + } + + + /** + * Validate the key + * + * @param string $key + * @return void + * @throws InvalidArgumentException + */ + protected function assertKey($key): void + { + if (!is_string($key)) { + $type = (is_object($key) ? get_class($key) . ' ' : '') . gettype($key); + throw new InvalidArgumentException("Expected key to be a string, not $type"); + } + + if ($key === '' || preg_match('~[{}()/\\\\@:]~', $key)) { + throw new InvalidArgumentException("Invalid key '$key'"); + } + } + + /** + * Assert that the keys are an array or traversable + * + * @param iterable $subject + * @param string $msg + * @return void + * @throws InvalidArgumentException if subject are not iterable + */ + protected function assertIterable($subject, $msg): void + { + $iterable = function_exists('is_iterable') + ? is_iterable($subject) + : is_array($subject) || $subject instanceof Traversable; + + if (!$iterable) { + throw new InvalidArgumentException($msg); + } + } + + /** + * Turn the key into a cache identifier + * + * @param string $key + * @return string + * @throws InvalidArgumentException + */ + protected function keyToId($key): string + { + $this->assertKey($key); + + return sprintf('%s%s', $this->prefix, $key); + } + + /** + * Create a map with keys and ids + * + * @param iterable $keys + * @return array + * @throws InvalidArgumentException + */ + protected function mapKeysToIds($keys): array + { + $this->assertIterable($keys, 'keys not iterable'); + + $map = []; + + foreach ($keys as $key) { + $id = $this->keyToId($key); + $map[$id] = $key; + } + + return $map; + } + + + /** + * Pack all values and turn keys into ids + * + * @param iterable $values + * @return array + */ + protected function packValues(iterable $values): array + { + $packed = []; + + foreach ($values as $key => $value) { + $id = $this->keyToId(is_int($key) ? (string)$key : $key); + $packed[$id] = $this->pack($value); + } + + return $packed; + } + + + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $this->assertIterable($keys, 'keys not iterable'); + + $result = []; + + foreach ($keys as $key) { + $result[$key] = $this->get($key, $default); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $this->assertIterable($values, 'values not iterable'); + + $success = true; + + foreach ($values as $key => $value) { + $success = $this->set(is_int($key) ? (string)$key : $key, $value, $ttl) && $success; + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $this->assertIterable($keys, 'keys not iterable'); + + $success = true; + + foreach ($keys as $key) { + $success = $this->delete($key) && $success; + } + + return $success; + } + + + /** + * Get the current time. + * + * @return int + */ + protected function currentTimestamp(): int + { + return time(); + } + + /** + * Convert TTL to seconds from now + * + * @param null|int|DateInterval $ttl + * @return int|null + * @throws InvalidArgumentException + */ + protected function ttlToSeconds($ttl): ?int + { + if (!isset($ttl)) { + return $this->ttl; + } + + if ($ttl instanceof DateInterval) { + $reference = new DateTimeImmutable(); + $endTime = $reference->add($ttl); + + $ttl = $endTime->getTimestamp() - $reference->getTimestamp(); + } + + if (!is_int($ttl)) { + $type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl); + throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type"); + } + + return isset($this->ttl) ? min($ttl, $this->ttl) : $ttl; + } + + /** + * Convert TTL to epoch timestamp + * + * @param null|int|DateInterval $ttl + * @return int|null + * @throws InvalidArgumentException + */ + protected function ttlToTimestamp($ttl): ?int + { + if (!isset($ttl)) { + return isset($this->ttl) ? time() + $this->ttl : null; + } + + if (is_int($ttl)) { + return time() + (isset($this->ttl) ? min($ttl, $this->ttl) : $ttl); + } + + if ($ttl instanceof DateInterval) { + $timestamp = (new DateTimeImmutable())->add($ttl)->getTimestamp(); + + return isset($this->ttl) ? min($timestamp, time() + $this->ttl) : $timestamp; + } + + $type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl); + throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type"); + } +} diff --git a/src/AbstractFile.php b/src/AbstractFile.php new file mode 100644 index 0000000..4132868 --- /dev/null +++ b/src/AbstractFile.php @@ -0,0 +1,188 @@ + + * @author Arnold Daniels + */ + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractCache; +use Desarrolla2\Cache\Exception\CacheException; +use Desarrolla2\Cache\Exception\InvalidArgumentException; +use Desarrolla2\Cache\Option\FilenameTrait as FilenameOption; +use Webmozart\Glob\Iterator\GlobIterator; + +/** + * Abstract class for using files as cache. + * + * @package Desarrolla2\Cache + */ +abstract class AbstractFile extends AbstractCache +{ + use FilenameOption; + + /** + * @var string + */ + protected $cacheDir; + + + /** + * Class constructor + * + * @param string|null $cacheDir + * @throws CacheException + */ + public function __construct(?string $cacheDir = null) + { + if (!$cacheDir) { + $cacheDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'cache'; + } + + $this->cacheDir = rtrim($cacheDir, '/'); + } + + /** + * Validate the key + * + * @param string $key + * @return void + * @throws InvalidArgumentException + */ + protected function assertKey($key): void + { + parent::assertKey($key); + + if (strpos($key, '*')) { + throw new InvalidArgumentException("Key may not contain the character '*'"); + } + } + + + /** + * Read the cache file + * + * @param $cacheFile + * @return string + */ + protected function readFile($cacheFile): string + { + return file_get_contents($cacheFile); + } + + /** + * Read the first line of the cache file + * + * @param string $cacheFile + * @return string + */ + protected function readLine(string $cacheFile): string + { + $fp = fopen($cacheFile, 'r'); + $line = fgets($fp); + fclose($fp); + + return $line; + } + + /** + * Create a cache file + * + * @param string $cacheFile + * @param string $contents + * @return bool + */ + protected function writeFile(string $cacheFile, string $contents): bool + { + $dir = dirname($cacheFile); + + if ($dir !== $this->cacheDir && !is_dir($dir)) { + mkdir($dir, 0775, true); + } + + return (bool)file_put_contents($cacheFile, $contents); + } + + /** + * Delete a cache file + * + * @param string $file + * @return bool + */ + protected function deleteFile(string $file): bool + { + return !is_file($file) || unlink($file); + } + + /** + * Recursive delete an empty directory. + * + * @return bool + */ + protected function removeFiles() + { + $generator = $this->getFilenameOption(); + $pattern = $this->cacheDir . DIRECTORY_SEPARATOR . $generator('*'); + + $objects = new GlobIterator($pattern); + + foreach ($objects as $object) { + unlink($object); + } + } + + /** + * Recursive delete an empty directory. + * + * @param string $dir + * @return bool + */ + protected function removeChildDirecotries(string $dir = null) + { + if (empty($dir)) { + $dir = $this->cacheDir; + } + + $success = true; + $objects = new GlobIterator($dir); + + foreach ($objects as $object) { + if (!is_dir("$dir/$object") && is_link("$dir/$object")) { + $success = $this->recursiveRemove("$dir/$object") && rmdir($dir) && $success; + } + } + + return $success; + } + + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $cacheFile = $this->getFilename($key); + + return $this->deleteFile($cacheFile); + } + + /** + * Delete cache directory. + * + * {@inheritdoc} + */ + public function clear() + { + $this->removeFiles(); + + return $this->removeChildDirecotries(); + } +} diff --git a/src/Adapter/AbstractAdapter.php b/src/Adapter/AbstractAdapter.php deleted file mode 100644 index b97ce76..0000000 --- a/src/Adapter/AbstractAdapter.php +++ /dev/null @@ -1,144 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use Desarrolla2\Cache\Exception\CacheException; - -/** - * AbstractAdapter - */ -abstract class AbstractAdapter implements AdapterInterface -{ - /** - * @var int - */ - protected $ttl = 3600; - - /** - * @var string - */ - protected $prefix = ''; - - /** - * @var bool - */ - protected $serialize = true; - - /** - * {@inheritdoc } - */ - public function setOption($key, $value) - { - switch ($key) { - case 'ttl': - $value = (int) $value; - if ($value < 1) { - throw new CacheException('ttl cant be lower than 1'); - } - $this->ttl = $value; - break; - case 'prefix': - $this->prefix = (string) $value; - break; - case 'serialize': - $this->serialize = (bool) $value; - break; - default: - throw new CacheException('option not valid '.$key); - } - - return true; - } - - /** - * {@inheritdoc } - */ - public function clearCache() - { - throw new Exception('not ready yet'); - } - - /** - * {@inheritdoc } - */ - public function dropCache() - { - throw new Exception('not ready yet'); - } - - /** - * - * @param string $key - * @return string - */ - protected function getKey($key) - { - //return md5($key); - return $key; - } - - /** - * Builds the key according to the prefix and other options - * - * @param string key - * @return string - */ - protected function buildKey($key) - { - return $this->prefix.$key; - } - - /** - * Packages the data to be stored by the internal caching driver - * according to the options on the adapter. - * - * @param mixed $data - * @return mixed - */ - protected function packData($data) - { - if ($this->serialize) { - return serialize($data); - } - - return $data; - } - - /** - * Unpackages the data retrieved by the internal caching driver - * according to the options on the adapter. This will be the inverse - * of packData IF the options are set correctly. - * - * @param mixed $data - * @return mixed - */ - protected function unpackData($data) - { - if ($this->serialize) { - return unserialize($data); - } - - return $data; - } - - protected function serialize($value) - { - return serialize($value); - } - - protected function unserialize($value) - { - return unserialize($value); - } -} diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php deleted file mode 100644 index 73be169..0000000 --- a/src/Adapter/AdapterInterface.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -/** - * Interface AdapterInterface - */ -interface AdapterInterface -{ - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key); - - /** - * Retrieve the value corresponding to a provided key - * - * @param string $key - * @return mixed - */ - public function get($key); - - /** - * Retrieve the if value corresponding to a provided key exist - * - * @param string $key - * @return bool - */ - public function has($key); - - /** - * * Add a value to the cache under a unique key - * - * @param string $key - * @param mixed $value - * @param int $ttl - */ - public function set($key, $value, $ttl = null); - - /** - * Set option for Adapter - * - * @param string $key - * @param string $value - */ - public function setOption($key, $value); - - /** - * clean all expired records from cache - */ - public function clearCache(); - - /** - * clear all cache - */ - public function dropCache(); -} diff --git a/src/Adapter/Apc.php b/src/Adapter/Apc.php deleted file mode 100644 index 68549e7..0000000 --- a/src/Adapter/Apc.php +++ /dev/null @@ -1,123 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use Desarrolla2\Cache\Exception\ApcCacheException; - -/** - * Apc - */ -class Apc extends AbstractAdapter -{ - - private $apcu; - - public function __construct() - { - $this->apcu = extension_loaded('apcu'); - } - - /** - * Delete a value from the cache - * - * @param string $key - * @throws \Desarrolla2\Cache\Exception\ApcCacheException - */ - public function delete($key) - { - if ($this->has($key)) { - $tKey = $this->getKey($key); - if (!($this->apcu ? \apcu_delete($tKey) : \apc_delete($tKey))) { - throw new ApcCacheException('Error deleting data with the key "'.$key.'" from the APC cache.'); - } - } - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - if ($this->has($key)) { - $tKey = $this->getKey($key); - if (!$data = ($this->apcu ? \apcu_fetch($tKey) : \apc_fetch($tKey))) { - throw new ApcCacheException('Error fetching data with the key "'.$key.'" from the APC cache.'); - } - - return $this->unserialize($data); - } - - return null; - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - $tKey = $this->getKey($key); - - if (function_exists("\apcu_exists") || function_exists("\apc_exists")) { - return (boolean) ($this->apcu ? \apcu_exists($tKey) : \apc_exists($tKey)); - } else { - $result = false; - ($this->apcu ? \apcu_fetch($tKey, $result) : \apc_fetch($tKey, $result)); - - return (boolean) $result; - } - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - $tKey = $this->getKey($key); - $tValue = $this->serialize($value); - if (!$ttl) { - $ttl = $this->ttl; - } - if (!($this->apcu ? \apcu_store($tKey, $tValue, $ttl) : \apc_store($tKey, $tValue, $ttl))) { - throw new ApcCacheException('Error saving data with the key "'.$key.'" to the APC cache.'); - } - } - - /** - * {@inheritdoc } - */ - public function setOption($key, $value) - { - switch ($key) { - case 'ttl': - $value = (int) $value; - if ($value < 1) { - throw new ApcCacheException('ttl cant be lower than 1'); - } - $this->ttl = $value; - break; - default: - throw new ApcCacheException('option not valid '.$key); - } - - return true; - } - - /** - * {@inheritdoc } - */ - public function dropCache() - { - ($this->apcu ? apcu_clear_cache("user") : apc_clear_cache("user")); - } -} diff --git a/src/Adapter/File.php b/src/Adapter/File.php deleted file mode 100644 index bbc5cd9..0000000 --- a/src/Adapter/File.php +++ /dev/null @@ -1,214 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use Desarrolla2\Cache\Exception\FileCacheException; - -/** - * File - */ -class File extends AbstractAdapter -{ - const CACHE_FILE_PREFIX = '__'; - const CACHE_FILE_SUBFIX = '.php.cache'; - - /** - * @var string - */ - protected $cacheDir; - - /** - * @param null $cacheDir - * @throws FileCacheException - */ - public function __construct($cacheDir = null) - { - if (!$cacheDir) { - $cacheDir = realpath(sys_get_temp_dir()).'/cache'; - } - $this->cacheDir = (string) $cacheDir; - if (!is_dir($this->cacheDir)) { - if (!mkdir($this->cacheDir, 0777, true)) { - throw new FileCacheException($this->cacheDir.' is not writable'); - } - } - if (!is_writable($this->cacheDir)) { - throw new FileCacheException($this->cacheDir.' is not writable'); - } - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $tKey = $this->getKey($key); - $cacheFile = $this->getCacheFile($tKey); - $this->deleteFile($cacheFile); - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - if ($data = $this->getData($key)) { - return $data; - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - if ($this->getData($key)) { - return true; - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - $tKey = $this->getKey($key); - $cacheFile = $this->getCacheFile($tKey); - $tValue = $this->serialize($value); - if (!($ttl)) { - $ttl = $this->ttl; - } - $item = $this->serialize( - array( - 'value' => $tValue, - 'ttl' => (int) $ttl + time(), - ) - ); - if (!file_put_contents($cacheFile, $item)) { - throw new FileCacheException('Error saving data with the key "'.$key.'" to the cache file.'); - } - } - - /** - * {@inheritdoc } - */ - public function setOption($key, $value) - { - switch ($key) { - case 'ttl': - $value = (int) $value; - if ($value < 1) { - throw new FileCacheException('ttl cant be lower than 1'); - } - $this->ttl = $value; - break; - default: - throw new FileCacheException('option not valid '.$key); - } - - return true; - } - - /** - * {@inheritdoc } - */ - public function clearCache() - { - throw new Exception('not ready yet'); - } - - /** - * {@inheritdoc } - */ - public function dropCache() - { - foreach (scandir($this->cacheDir) as $fileName) { - $cacheFile = $this->cacheDir. - DIRECTORY_SEPARATOR. - $fileName; - $this->deleteFile($cacheFile); - } - } - - /** - * Delete file - * - * @param string $cacheFile - * @return bool - */ - protected function deleteFile($cacheFile) - { - if (is_file($cacheFile)) { - return unlink($cacheFile); - } - - return false; - } - - /** - * Get the specified cache file - */ - protected function getCacheFile($fileName) - { - return $this->cacheDir. - DIRECTORY_SEPARATOR. - self::CACHE_FILE_PREFIX. - $fileName. - self::CACHE_FILE_SUBFIX; - } - - /** - * Get data value from file cache - * - * @param string $key - * @return mixed - * @throws FileCacheException - */ - protected function getData($key) - { - $tKey = $this->getKey($key); - $cacheFile = $this->getCacheFile($tKey); - if (!file_exists($cacheFile)) { - return; - } - if (!$data = unserialize(file_get_contents($cacheFile))) { - throw new FileCacheException( - 'Error with the key "'.$key.'" in cache file '.$cacheFile - ); - } - if (!array_key_exists('value', $data)) { - throw new FileCacheException( - 'Error with the key "'.$key.'" in cache file '.$cacheFile.', value not exist' - ); - } - if (!array_key_exists('ttl', $data)) { - throw new FileCacheException( - 'Error with the key "'.$key.'" in cache file '.$cacheFile.', ttl not exist' - ); - } - if (time() > $data['ttl']) { - $this->delete($key); - - return; - } - - return $this->unserialize($data['value']); - } -} diff --git a/src/Adapter/MemCache.php b/src/Adapter/MemCache.php deleted file mode 100644 index 433aac0..0000000 --- a/src/Adapter/MemCache.php +++ /dev/null @@ -1,97 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use \Memcache as BaseMemCache; - -/** - * MemCache - */ -class MemCache extends AbstractAdapter -{ - /** - * - * @var \Memcache - */ - protected $server; - - public function __construct() - { - $this->server = new BaseMemcache(); - $this->server->addServer('localhost', 11211); - } - - /** - * - * @param string $host - * @param string $port - */ - public function addServer($host, $port) - { - $this->server->addServer($host, $port); - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $tKey = $this->getKey($key); - $this->server->delete($tKey); - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - $tKey = $this->getKey($key); - $data = $this->server->get($tKey); - - return $this->unserialize($data); - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - $tKey = $this->getKey($key); - if ($this->server->get($tKey)) { - return true; - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - $tKey = $this->getKey($key); - $tValue = $this->serialize($value); - if (!$ttl) { - $ttl = $this->ttl; - } - $this->server->set($tKey, $tValue, false, time() + $ttl); - } - - public function dropCache($delay = 0) - { - $this->server->flush($delay); - } -} diff --git a/src/Adapter/Memcached.php b/src/Adapter/Memcached.php deleted file mode 100644 index 50daadb..0000000 --- a/src/Adapter/Memcached.php +++ /dev/null @@ -1,127 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use \Memcached as BaseMemcached; - -/** - * Memcached - * - * @author : RJ Garcia - */ -class Memcached extends AbstractAdapter -{ - /** - * - * @var \Memcached - */ - public $adapter; - - /** - * Accept three types of inputs: a \Memcached instance already to use - * as the internal adapter, an array of servers which will be added to - * a memcached instance, or null, and we'll just build a default memcached - * instance - * - * @param mixed $data - * - */ - public function __construct($data = null) - { - if ($data instanceof BaseMemcached) { - $this->adapter = $data; - } else { - $this->adapter = new BaseMemcached(); - } - - if (is_array($data)) { - /* if array, then the user supplied an array of servers */ - foreach ($data as $s) { - $this->addServer($s['host'], $s['port'], $s['weight']); - } - } - } - - /** - * - * @param string $host - * @param int $port - * @param int $weight - * - */ - public function addServer($host, $port, $weight = 0) - { - $this->adapter->addServer($host, $port, $weight); - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $this->adapter->delete($this->buildKey($key)); - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - return $this->unpackData( - $this->adapter->get( - $this->buildKey($key) - ) - ); - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - /* It seems that the most efficient way to check has in memcached is - by using an append with an empty string. However, we need to make - sure that OPT_COMPRESSION is turned off because you can't append - if you compressing data */ - - /* store for later use */ - $cur_compression = $this->adapter->getOption(BaseMemcached::OPT_COMPRESSION); - - /* set compression off */ - $this->adapter->setOption(BaseMemcached::OPT_COMPRESSION, false); - - $res = $this->adapter->append( - $this->buildKey($key), - '' - ); - - $this->adapter->setOption(BaseMemcached::OPT_COMPRESSION, $cur_compression); - - return $res; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - $this->adapter->set( - $this->buildKey($key), - $this->packData($value), - $ttl ?: $this->ttl - ); - } -} diff --git a/src/Adapter/Memory.php b/src/Adapter/Memory.php deleted file mode 100644 index 8617936..0000000 --- a/src/Adapter/Memory.php +++ /dev/null @@ -1,112 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -/** - * Memory - */ -class Memory extends AbstractAdapter -{ - /** - * - * @var int - */ - protected $limit = 100; - - /** - * - * @var array - */ - protected $cache = array(); - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $tKey = $this->getKey($key); - unset($this->cache[$tKey]); - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - if ($this->has($key)) { - $tKey = $this->getKey($key); - - return $this->unserialize($this->cache[$tKey]['value']); - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - $tKey = $this->getKey($key); - if (isset($this->cache[$tKey])) { - $data = $this->cache[$tKey]; - if (time() < $data['ttl']) { - return true; - } else { - $this->delete($key); - } - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - while (count($this->cache) >= $this->limit) { - array_shift($this->cache); - } - $tKey = $this->getKey($key); - if (!$ttl) { - $ttl = $this->ttl; - } - $this->cache[$tKey] = array( - 'value' => serialize($value), - 'ttl' => $ttl + time(), - ); - } - - /** - * {@inheritdoc } - */ - public function setOption($key, $value) - { - switch ($key) { - case 'limit': - $value = (int) $value; - if ($value < 1) { - throw new MemoryCacheException('limit cant be lower than 1'); - } - $this->limit = $value; - - return true; - } - - return parent::setOption($key, $value); - } -} diff --git a/src/Adapter/Mongo.php b/src/Adapter/Mongo.php deleted file mode 100644 index 530c342..0000000 --- a/src/Adapter/Mongo.php +++ /dev/null @@ -1,167 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use Desarrolla2\Cache\Exception\MongoCacheException; -use Mongo as MongoBase; - -/** - * Mongo - */ -class Mongo extends AbstractAdapter -{ - /** - * @var \MongoDB - */ - protected $database; - - /** - * @var \Mongo - */ - protected $mongo; - - /** - * - * @param string $server - * @param array $options - * @param string $database - * @throws \Desarrolla2\Cache\Exception\MongoCacheException - */ - public function __construct( - $server = 'mongodb://localhost:27017', - $options = array('connect' => true), - $database = '__cache' - ) { - $this->mongo = new MongoBase($server, $options); - if (!$this->mongo) { - throw new MongoCacheException(' Mongo connection fails '); - } - $this->database = $this->mongo->selectDB($database); - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $tKey = $this->getKey($key); - $this->database->items->remove(array('key' => $tKey)); - } - - /** - * {@inheritdoc } - * - * @param string $key - */ - public function get($key) - { - if ($data = $this->getData($key)) { - return $data; - } - - return false; - } - - /** - * {@inheritdoc } - * - * @param string $key - * @return bool - */ - public function has($key) - { - if ($this->getData($key)) { - return true; - } - - return false; - } - - /** - * {@inheritdoc } - * - * @param string $key - * @param mixed $value - * @param null $ttl - */ - public function set($key, $value, $ttl = null) - { - $tKey = $this->getKey($key); - $tValue = $this->serialize($value); - if (!$ttl) { - $ttl = $this->ttl; - } - $item = array( - 'key' => $tKey, - 'value' => $tValue, - 'ttl' => (int) $ttl + time(), - ); - $this->delete($key); - $this->database->items->insert($item); - } - - /** - * {@inheritdoc } - * - * @param string $key - * @param string $value - * @return bool - * @throws \Desarrolla2\Cache\Exception\MongoCacheException - */ - public function setOption($key, $value) - { - switch ($key) { - case 'ttl': - $value = (int) $value; - if ($value < 1) { - throw new MongoCacheException('ttl cant be lower than 1'); - } - $this->ttl = $value; - break; - default: - throw new MongoCacheException('option not valid '.$key); - } - - return true; - } - - /** - * Get data value from file cache - * - * @param string $key - * @param bool $delete - * @return mixed - */ - protected function getData($key, $delete = true) - { - $tKey = $this->getKey($key); - $data = $this->database->items->findOne(array('key' => $tKey)); - if (count($data)) { - $data = array_values($data); - if (time() > $data[3]) { - if ($delete) { - $this->delete($key); - } - - return false; - } - - return $this->unserialize($data[2]); - } - - return false; - } -} diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php deleted file mode 100644 index 51e9118..0000000 --- a/src/Adapter/MySQL.php +++ /dev/null @@ -1,168 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use \mysqli; - -/** - * MySQL - */ -class MySQL extends AbstractAdapter implements AdapterInterface -{ - /** - * - * @var \mysqli - */ - protected $mysql; - - public function __construct( - $host = 'localhost', - $user = 'root', - $password = '', - $database = 'cache', - $port = '3306' - ) { - $this->mysql = new mysqli($host, $user, $password, $database, $port); - } - - /** - * Destructor - */ - public function __destruct() - { - $this->mysql->close(); - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $tKey = $this->getKey($key); - $query = 'DELETE FROM cache WHERE hash = \''.$tKey.'\';'; - - $this->query($query); - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - $tKey = $this->getKey($key); - $query = 'SELECT value FROM cache WHERE hash = \''.$tKey.'\''. - ' AND ttl >= '.time().';'; - $res = $this->fetchObject($query); - if ($res) { - return $this->unserialize($res->value); - } - - return false; - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - $tKey = $this->getKey($key); - $query = 'SELECT COUNT(*) AS items FROM cache WHERE hash = '. - '\''.$tKey.'\' AND '. - ' ttl >= '.time().';'; - $res = $this->fetchObject($query); - if (!$res) { - return false; - } - if ($res->items == '0') { - return false; - } - - return true; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - $this->delete($key); - $tKey = $this->getKey($key); - $tValue = $this->escape( - $this->serialize($value) - ); - if (!($ttl)) { - $ttl = $this->ttl; - } - $tTtl = $ttl + time(); - $query = ' INSERT INTO cache (hash, value, ttl) VALUES ('. - '\''.$tKey.'\', '. - '\''.$tValue.'\', '. - '\''.$tTtl.'\' );'; - $this->query($query); - } - - /** - * {@inheritdoc } - */ - protected function getKey($key) - { - $tKey = parent::getKey($key); - - return $this->escape($tKey); - } - - /** - * - * @param string $query - * @param int|string $mode - * @return mixed - */ - protected function fetchObject($query, $mode = MYSQLI_STORE_RESULT) - { - $res = $this->query($query, $mode); - if ($res) { - return $res->fetch_object(); - } - - return false; - } - - /** - * - * @param string $query - * @param int|string $mode - * @return mixed - */ - protected function query($query, $mode = MYSQLI_STORE_RESULT) - { - $res = $this->mysql->query($query, $mode); - if ($res) { - return $res; - } - - return false; - } - - /** - * - * @param string $key - * @return string - */ - private function escape($key) - { - return $this->mysql->real_escape_string($key); - } -} diff --git a/src/Adapter/NotCache.php b/src/Adapter/NotCache.php deleted file mode 100644 index c920b82..0000000 --- a/src/Adapter/NotCache.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -/** - * NotCache - */ -class NotCache extends AbstractAdapter -{ - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - } - - /** - * {@inheritdoc } - */ - public function get($key) - { - return false; - } - - /** - * {@inheritdoc } - */ - public function has($key) - { - return false; - } - - /** - * {@inheritdoc } - */ - public function set($key, $value, $ttl = null) - { - return false; - } - - /** - * {@inheritdoc } - */ - public function setOption($key, $value) - { - return false; - } -} diff --git a/src/Adapter/Redis.php b/src/Adapter/Redis.php deleted file mode 100644 index c2d496f..0000000 --- a/src/Adapter/Redis.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter; - -use Predis\Client; - -/** - * Redis - */ -class Redis extends AbstractAdapter -{ - /** - * @var Client - */ - protected $client; - - public function __construct() - { - $this->client = new Client(); - } - - public function __destruct() - { - $this->client->disconnect(); - } - - /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key) - { - $cmd = $this->client->createCommand('DEL'); - $cmd->setArguments(array($key)); - - $this->client->executeCommand($cmd); - } - - /** - * Retrieve the value corresponding to a provided key - * - * @param string $key - * @return mixed - */ - public function get($key) - { - return $this->client->get($key); - } - - /** - * Retrieve the if value corresponding to a provided key exist - * - * @param string $key - * @return bool - */ - public function has($key) - { - $cmd = $this->client->createCommand('EXISTS'); - $cmd->setArguments(array($key)); - - return $this->client->executeCommand($cmd); - } - - /** - * * Add a value to the cache under a unique key - * - * @param string $key - * @param mixed $value - * @param int $ttl - */ - public function set($key, $value, $ttl = null) - { - $this->client->set($key, $value); - if ($ttl) { - $cmd = $this->client->createCommand('EXPIRE'); - $cmd->setArguments(array($key, $ttl)); - $this->client->executeCommand($cmd); - } - } -} diff --git a/src/Apcu.php b/src/Apcu.php new file mode 100644 index 0000000..9cdfab5 --- /dev/null +++ b/src/Apcu.php @@ -0,0 +1,88 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\Exception\CacheException; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\NopPacker; + +/** + * Apcu + */ +class Apcu extends AbstractCache +{ + /** + * Create the default packer for this cache implementation + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new NopPacker(); + } + + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $ttlSeconds = $this->ttlToSeconds($ttl); + + if (isset($ttlSeconds) && $ttlSeconds <= 0) { + return $this->delete($key); + } + + return apcu_store($this->keyToId($key), $this->pack($value), $ttlSeconds ?? 0); + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $packed = apcu_fetch($this->keyToId($key), $success); + + return $success ? $this->unpack($packed) : $default; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return apcu_exists($this->keyToId($key)); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $id = $this->keyToId($key); + + return apcu_delete($id) || !apcu_exists($id); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return apcu_clear_cache(); + } +} diff --git a/src/Cache.php b/src/Cache.php deleted file mode 100644 index 8eb12f9..0000000 --- a/src/Cache.php +++ /dev/null @@ -1,131 +0,0 @@ - - */ - -namespace Desarrolla2\Cache; - -use Desarrolla2\Cache\Adapter\AdapterInterface; -use Desarrolla2\Cache\Exception\AdapterNotSetException; - -/** - * Cache - */ -class Cache implements CacheInterface -{ - /** - * - * @var Adapter\AdapterInterface - */ - protected $adapter; - - /** - * @param AdapterInterface $adapter - */ - public function __construct(AdapterInterface $adapter = null) - { - if ($adapter) { - $this->setAdapter($adapter); - } - } - - /** - * {@inheritdoc } - * - * @param string $key - */ - public function delete($key) - { - $this->getAdapter()->delete($key); - } - - /** - * {@inheritdoc } - * - * @param string $key - */ - public function get($key) - { - return $this->getAdapter()->get($key); - } - - /** - * {@inheritdoc } - */ - public function getAdapter() - { - if (!$this->adapter) { - throw new AdapterNotSetException('Required Adapter'); - } - - return $this->adapter; - } - - /** - * {@inheritdoc } - * - * @param string $key - */ - public function has($key) - { - return $this->getAdapter()->has($key); - } - - /** - * {@inheritdoc } - * - * @param string $key - * @param mixed $value - * @param null $ttl - */ - public function set($key, $value, $ttl = null) - { - $this->getAdapter()->set($key, $value, $ttl); - } - - /** - * {@inheritdoc } - * - * @param Adapter\AdapterInterface $adapter - */ - public function setAdapter(AdapterInterface $adapter) - { - $this->adapter = $adapter; - } - - /** - * {@inheritdoc } - * - * @param string $key - * @param string $value - * @return mixed - */ - public function setOption($key, $value) - { - return $this->adapter->setOption($key, $value); - } - - /** - * {@inheritdoc } - */ - public function clearCache() - { - $this->adapter->clearCache(); - } - - /** - * {@inheritdoc } - */ - public function dropCache() - { - $this->adapter->dropCache(); - } -} diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 2f5208d..00d5d46 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -9,74 +9,50 @@ * file that was distributed with this source code. * * @author Daniel González + * @author Arnold Daniels */ namespace Desarrolla2\Cache; +use Psr\SimpleCache\CacheInterface as PsrCacheInterface; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\KeyMaker\KeyMakerInterface; + /** * CacheInterface */ -interface CacheInterface +interface CacheInterface extends PsrCacheInterface { /** - * Delete a value from the cache - * - * @param string $key - */ - public function delete($key); - - /** - * Retrieve the value corresponding to a provided key + * Set option for cache * * @param string $key + * @param mixed $value + * @return static */ - public function get($key); + public function withOption(string $key, $value); /** + * Set multiple options for cache * - * @return \Desarrolla2\Cache\Adapter\AdapterInterface $adapter - * @throws Exception - */ - public function getAdapter(); - - /** - * Retrieve the if value corresponding to a provided key exist - * - * @param string $key + * @param array $options + * @return static */ - public function has($key); + public function withOptions(array $options); /** - * * Add a value to the cache under a unique key + * Get option for cache * * @param string $key - * @param mixed $value - * @param int $ttl + * @return mixed */ - public function set($key, $value, $ttl = null); + public function getOption($key); /** - * Set Adapter interface + * Set the packer * - * @param \Desarrolla2\Cache\Adapter\AdapterInterface $adapter - */ - public function setAdapter(\Desarrolla2\Cache\Adapter\AdapterInterface $adapter); - - /** - * Set option for Adapter - * - * @param string $key - * @param string $value - */ - public function setOption($key, $value); - - /** - * clean all expired records from cache - */ - public function clearCache(); - - /** - * clear all cache + * @param PackerInterface $packer + * @return static */ - public function dropCache(); + public function withPacker(PackerInterface $packer); } diff --git a/src/Chain.php b/src/Chain.php new file mode 100644 index 0000000..cc9ea2c --- /dev/null +++ b/src/Chain.php @@ -0,0 +1,190 @@ + + * @author Arnold Daniels + */ + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractCache; +use Desarrolla2\Cache\CacheInterface; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * Use multiple cache adapters. + */ +class Chain extends AbstractCache +{ + /** + * @var CacheInterface[] + */ + protected $adapters; + + /** + * Create the default packer for this cache implementation + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new NopPacker(); + } + + + /** + * Chain constructor. + * + * @param CacheInterface[] $adapters Fastest to slowest + */ + public function __construct(array $adapters) + { + foreach ($adapters as $adapter) { + if (!$adapter instanceof CacheInterface) { + throw new InvalidArgumentException("All adapters should be a cache implementation"); + } + } + + $this->adapters = $adapters; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $success = true; + + foreach ($this->adapters as $adapter) { + $success = $adapter->set($key, $value, $ttl) && $success; + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $success = true; + + foreach ($this->adapters as $adapter) { + $success = $adapter->setMultiple($values, $ttl) && $success; + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + foreach ($this->adapters as $adapter) { + $result = $adapter->get($key); // Not using $default as we want to get null if the adapter doesn't have it + + if (isset($result)) { + return $result; + } + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $this->assertIterable($keys, 'keys are not iterable'); + + $missing = []; + $values = []; + + foreach ($keys as $key) { + $this->assertKey($key); + + $missing[] = $key; + $values[$key] = $default; + } + + foreach ($this->adapters as $adapter) { + if (empty($missing)) { + break; + } + + $found = array_filter($adapter->getMultiple($missing), function($value) { + return isset($value); + }); + + $values = array_merge($values, $found); + $missing = array_diff($missing, array_keys($found)); + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + foreach ($this->adapters as $adapter) { + if ($adapter->has($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $success = true; + + foreach ($this->adapters as $adapter) { + $success = $adapter->delete($key) && $success; + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $success = true; + + foreach ($this->adapters as $adapter) { + $success = $adapter->deleteMultiple($keys) && $success; + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $success = true; + + foreach ($this->adapters as $adapter) { + $success = $adapter->clear() && $success; + } + + return $success; + } +} diff --git a/src/Exception/ApcCacheException.php b/src/Exception/ApcCacheException.php deleted file mode 100644 index 99232fc..0000000 --- a/src/Exception/ApcCacheException.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Exception; - -/** - * ApcCacheException - */ -class ApcCacheException extends \Exception -{ -} diff --git a/src/Exception/FileCacheException.php b/src/Exception/BadMethodCallException.php similarity index 56% rename from src/Exception/FileCacheException.php rename to src/Exception/BadMethodCallException.php index ef3a956..079f2c1 100644 --- a/src/Exception/FileCacheException.php +++ b/src/Exception/BadMethodCallException.php @@ -1,6 +1,5 @@ + * @author Arnold Daniels */ +declare(strict_types=1); + namespace Desarrolla2\Cache\Exception; +use Psr\SimpleCache\CacheException as PsrCacheException; + /** - * FileCacheException + * Exception bad method calls */ -class FileCacheException extends \Exception +class BadMethodCallException extends \BadMethodCallException implements PsrCacheException { } diff --git a/src/Exception/CacheException.php b/src/Exception/CacheException.php index 717d0b2..079df45 100644 --- a/src/Exception/CacheException.php +++ b/src/Exception/CacheException.php @@ -1,6 +1,5 @@ + * @author Arnold Daniels */ +declare(strict_types=1); + namespace Desarrolla2\Cache\Exception; +use Psr\SimpleCache\CacheException as PsrCacheException; + /** - * CacheException + * Interface used for all types of exceptions thrown by the implementing library. */ -class CacheException extends \Exception +class CacheException extends \RuntimeException implements PsrCacheException { } diff --git a/src/Exception/AdapterNotSetException.php b/src/Exception/InvalidArgumentException.php similarity index 52% rename from src/Exception/AdapterNotSetException.php rename to src/Exception/InvalidArgumentException.php index 7f1d439..fd155e5 100644 --- a/src/Exception/AdapterNotSetException.php +++ b/src/Exception/InvalidArgumentException.php @@ -1,6 +1,5 @@ + * @author Arnold Daniels */ +declare(strict_types=1); + namespace Desarrolla2\Cache\Exception; +use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException; + /** - * AdapterNotSetException + * Exception for invalid cache arguments. */ -class AdapterNotSetException extends \Exception +class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException { } diff --git a/src/Exception/MemoryCacheException.php b/src/Exception/MemoryCacheException.php deleted file mode 100644 index 98cdb81..0000000 --- a/src/Exception/MemoryCacheException.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Exception; - -/** - * MemoryCacheException - */ -class MemoryCacheException extends \Exception -{ -} diff --git a/src/Exception/MongoCacheException.php b/src/Exception/MongoCacheException.php deleted file mode 100644 index 3108995..0000000 --- a/src/Exception/MongoCacheException.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Exception; - -/** - * MongoCacheException - */ -class MongoCacheException extends \Exception -{ -} diff --git a/src/Exception/MySQLCacheException.php b/src/Exception/MySQLCacheException.php deleted file mode 100644 index 41e5b58..0000000 --- a/src/Exception/MySQLCacheException.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Exception; - -/** - * MySQLCacheException - */ -class MySQLCacheException extends \Exception -{ -} diff --git a/src/Exception/MemcacheException.php b/src/Exception/UnexpectedValueException.php similarity index 53% rename from src/Exception/MemcacheException.php rename to src/Exception/UnexpectedValueException.php index 06ee948..2c05cc4 100644 --- a/src/Exception/MemcacheException.php +++ b/src/Exception/UnexpectedValueException.php @@ -1,6 +1,5 @@ + * @author Arnold Daniels */ +declare(strict_types=1); + namespace Desarrolla2\Cache\Exception; +use Psr\SimpleCache\CacheException as PsrCacheException; + /** - * MemcacheException + * Exception for unexpected values when reading from cache. */ -class MemcacheException extends \Exception +class UnexpectedValueException extends \UnexpectedValueException implements PsrCacheException { } diff --git a/src/File.php b/src/File.php new file mode 100644 index 0000000..88524d2 --- /dev/null +++ b/src/File.php @@ -0,0 +1,174 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractFile; +use Desarrolla2\Cache\Exception\InvalidArgumentException; +use Desarrolla2\Cache\Exception\UnexpectedValueException; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\SerializePacker; + +/** + * Cache file. + */ +class File extends AbstractFile +{ + /** + * @var string 'embed', 'file', 'mtime' + */ + protected $ttlStrategy = 'embed'; + + /** + * Create the default packer for this cache implementation + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new SerializePacker(); + } + + /** + * Set TTL strategy + * + * @param string $strategy + */ + protected function setTtlStrategyOption($strategy) + { + if (!in_array($strategy, ['embed', 'file', 'mtime'])) { + throw new InvalidArgumentException("Unknown strategy '$strategy', should be 'embed', 'file' or 'mtime'"); + } + + $this->ttlStrategy = $strategy; + } + + /** + * Get TTL strategy + * + * @return bool + */ + protected function getTtlStrategyOption() + { + return $this->useTtlFile; + } + + + /** + * Get the TTL using one of the strategies + * + * @param string $cacheFile + * @return int + */ + protected function getTtl(string $cacheFile) + { + switch ($this->ttlStrategy) { + case 'embed': + return (int)$this->readLine($cacheFile); + case 'file': + return file_exists("$cacheFile.ttl") + ? (int)file_get_contents("$cacheFile.ttl") + : PHP_INT_MAX; + case 'mtime': + return $this->getTtl() > 0 ? filemtime($cacheFile) + $this->ttl : PHP_INT_MAX; + } + } + + /** + * Set the TTL using one of the strategies + * + * @param int $expiration + * @param string $contents + * @param string $cacheFile + * @return string The (modified) contents + */ + protected function setTtl($expiration, $contents, $cacheFile) + { + switch ($this->ttlStrategy) { + case 'embed': + $contents = ($expiration ?? PHP_INT_MAX) . "\n" . $contents; + break; + case 'file': + if (isset($expiration)) { + file_put_contents("$cacheFile.ttl", $expiration); + } + break; + case 'mtime': + // nothing + break; + } + + return $contents; + } + + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + if (!$this->has($key)) { + return $default; + } + + $cacheFile = $this->getFilename($key); + $packed = $this->readFile($cacheFile); + + if ($this->ttlStrategy === 'embed') { + $packed = substr($packed, strpos($packed, "\n") + 1); + } + + return $this->unpack($packed); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + $cacheFile = $this->getFilename($key); + + if (!file_exists($cacheFile)) { + return false; + } + + $ttl = $this->getTtl($cacheFile); + + if ($ttl <= time()) { + $this->deleteFile($cacheFile); + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $cacheFile = $this->getFilename($key); + $packed = $this->pack($value); + + if (!is_string($packed)) { + throw new UnexpectedValueException("Packer must create a string for the data to be cached to file"); + } + + $contents = $this->setTtl($this->ttlToTimestamp($ttl), $packed, $cacheFile); + + return $this->writeFile($cacheFile, $contents); + } +} diff --git a/src/File/BasicFilename.php b/src/File/BasicFilename.php new file mode 100644 index 0000000..1c7130c --- /dev/null +++ b/src/File/BasicFilename.php @@ -0,0 +1,68 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\File; + +/** + * Create a path for a key + */ +class BasicFilename +{ + /** + * @var string + */ + protected $format; + + /** + * BasicFilename constructor. + * + * @param string $format + */ + public function __construct(string $format) + { + $this->format = $format; + } + + /** + * Get the format + * + * @return string + */ + public function getFormat(): string + { + return $this->format; + } + + /** + * Create the path for a key + * + * @param string $key + * @return string + */ + public function __invoke(string $key): string + { + return sprintf($this->format, $key ?: '*'); + } + + /** + * Cast to string + * + * @return string + */ + public function __toString(): string + { + return $this->getFormat(); + } +} \ No newline at end of file diff --git a/src/File/TrieFilename.php b/src/File/TrieFilename.php new file mode 100644 index 0000000..6219009 --- /dev/null +++ b/src/File/TrieFilename.php @@ -0,0 +1,121 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\File; + +/** + * Create a path for a key as prefix tree directory structure. + * + * @see https://en.wikipedia.org/wiki/Trie + */ +class TrieFilename +{ + /** + * @var string + */ + protected $format; + + /** + * @var int + */ + protected $levels; + + /** + * @var bool + */ + protected $hash; + + + /** + * TrieFilename constructor. + * + * @param string $format + * @param int $levels The depth of the structure + * @param bool $hash MD5 hash the key to get a better spread + */ + public function __construct(string $format, int $levels = 1, bool $hash = false) + { + $this->format = $format; + $this->levels = $levels; + $this->hash = $hash; + } + + /** + * Get the format + * + * @return string + */ + public function getFormat(): string + { + return $this->format; + } + + /** + * Get the depth of the structure + * + * @return int + */ + public function getLevels(): int + { + return $this->levels; + } + + /** + * Will the key be hashed to create the trie. + * + * @return bool + */ + public function isHashed(): bool + { + return $this->hash; + } + + + /** + * Create the path for a key + * + * @param string $key + * @return string + */ + public function __invoke(string $key): string + { + if (empty($key)) { + return $this->wildcardPath(); + } + + $dirname = $this->hash ? base_convert(md5($key), 16, 36) : $key; + $filename = sprintf($this->format, $key); + + $path = ''; + + for ($length = 1; $length <= $this->levels; $length++) { + $path .= substr($dirname, 0, $length) . DIRECTORY_SEPARATOR; + } + + return $path . $filename; + } + + /** + * Get a path for all files (using glob) + * + * @return string + */ + protected function wildcardPath(): string + { + $filename = sprintf($this->format, '*'); + + return str_repeat('*' . DIRECTORY_SEPARATOR, $this->levels) . $filename; + } +} diff --git a/src/Memcached.php b/src/Memcached.php new file mode 100644 index 0000000..3b34974 --- /dev/null +++ b/src/Memcached.php @@ -0,0 +1,218 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\Exception\InvalidArgumentException; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\NopPacker; +use Memcached as MemcachedServer; + +/** + * Memcached + */ +class Memcached extends AbstractCache +{ + /** + * @var Server + */ + protected $server; + + /** + * @param MemcachedServer|null $server + */ + public function __construct(MemcachedServer $server) + { + $this->server = $server; + } + + + /** + * Create the default packer for this cache implementation + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new NopPacker(); + } + + /** + * Validate the key + * + * @param string $key + * @return void + * @throws InvalidArgumentException + */ + protected function assertKey($key): void + { + parent::assertKey($key); + + if (strlen($key) > 250) { + throw new InvalidArgumentException("Key to long, max 250 characters"); + } + } + + /** + * Pack all values and turn keys into ids + * + * @param iterable $values + * @return array + */ + protected function packValues(iterable $values): array + { + $packed = []; + + foreach ($values as $key => $value) { + $this->assertKey(is_int($key) ? (string)$key : $key); + $packed[$key] = $this->pack($value); + } + + return $packed; + } + + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $this->assertKey($key); + + $data = $this->server->get($key); + + if ($this->server->getResultCode() !== MemcachedServer::RES_SUCCESS) { + return $default; + } + + return $this->unpack($data); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + $this->assertKey($key); + $this->server->get($key); + + $result = $this->server->getResultCode(); + + return $result === MemcachedServer::RES_SUCCESS; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $this->assertKey($key); + + $packed = $this->pack($value); + $ttlTime = $this->ttlToMemcachedTime($ttl); + + if ($ttlTime === false) { + return $this->delete($key); + } + + $success = $this->server->set($key, $packed, $ttlTime); + + return $success; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $this->server->delete($this->keyToId($key)); + + $result = $this->server->getResultCode(); + + return $result === MemcachedServer::RES_SUCCESS || $result === MemcachedServer::RES_NOTFOUND; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $this->assertIterable($keys, 'keys not iterable'); + $keysArr = is_array($keys) ? $keys : iterator_to_array($keys, false); + array_walk($keysArr, [$this, 'assertKey']); + + $result = $this->server->getMulti($keysArr); + + if ($result === false) { + return false; + } + + $items = array_fill_keys($keysArr, $default); + + foreach ($result as $key => $value) { + $items[$key] = $this->unpack($value); + } + + return $items; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $this->assertIterable($values, 'values not iterable'); + + $packed = $this->packValues($values); + $ttlTime = $this->ttlToMemcachedTime($ttl); + + if ($ttlTime === false) { + return $this->server->deleteMulti(array_keys($packed)); + } + + return $this->server->setMulti($packed, $ttlTime); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->server->flush(); + } + + + /** + * Convert ttl to timestamp or seconds. + * + * @see http://php.net/manual/en/memcached.expiration.php + * + * @param null|int|DateInterval $ttl + * @return int|null + * @throws InvalidArgumentException + */ + protected function ttlToMemcachedTime($ttl) + { + $seconds = $this->ttlToSeconds($ttl); + + if ($seconds <= 0) { + return isset($seconds) ? false : 0; + } + + /* 2592000 seconds = 30 days */ + return $seconds <= 2592000 ? $seconds : $this->ttlToTimestamp($ttl); + } +} diff --git a/src/Memory.php b/src/Memory.php new file mode 100644 index 0000000..899cc79 --- /dev/null +++ b/src/Memory.php @@ -0,0 +1,165 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\SerializePacker; + +/** + * Memory + */ +class Memory extends AbstractCache +{ + /** + * Limit the amount of entries + * @var int + */ + protected $limit = PHP_INT_MAX; + + + /** + * @var array + */ + protected $cache = []; + + /** + * @var array + */ + protected $cacheTtl = []; + + + /** + * Create the default packer for this cache implementation. + * {@internal NopPacker might fail PSR-16, as cached objects would change} + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new SerializePacker(); + } + + /** + * Make a clone of this object. + * Set by cache reference, thus using the same pool. + * + * @return static + */ + protected function cloneSelf(): AbstractCache + { + $clone = clone $this; + + $clone->cache =& $this->cache; + $clone->cacheTtl =& $this->cacheTtl; + + return $clone; + } + + /** + * Set the max number of items + * + * @param int $limit + */ + protected function setLimitOption($limit) + { + $this->limit = (int)$limit ?: PHP_INT_MAX; + } + + /** + * Get the max number of items + * + * @return int + */ + protected function getLimitOption() + { + return $this->limit; + } + + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + if (!$this->has($key)) { + return $default; + } + + $id = $this->keyToId($key); + + return $this->unpack($this->cache[$id]); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + $id = $this->keyToId($key); + + if (!isset($this->cacheTtl[$id])) { + return false; + } + + if ($this->cacheTtl[$id] <= time()) { + unset($this->cache[$id], $this->cacheTtl[$id]); + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + if (count($this->cache) >= $this->limit) { + $deleteKey = key($this->cache); + unset($this->cache[$deleteKey], $this->cacheTtl[$deleteKey]); + } + + $id = $this->keyToId($key); + + $this->cache[$id] = $this->pack($value); + $this->cacheTtl[$id] = $this->ttlToTimestamp($ttl) ?? PHP_INT_MAX; + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $id = $this->keyToId($key); + unset($this->cache[$id], $this->cacheTtl[$id]); + + return true; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->cache = []; + $this->cacheTtl = []; + + return true; + } +} diff --git a/src/MongoDB.php b/src/MongoDB.php new file mode 100644 index 0000000..863697c --- /dev/null +++ b/src/MongoDB.php @@ -0,0 +1,273 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\MongoDBBinaryPacker; +use Desarrolla2\Cache\Option\InitializeTrait as InitializeOption; +use MongoDB\Collection; +use MongoDB\BSON\UTCDatetime as BSONUTCDateTime; +use MongoDB\Driver\Exception\RuntimeException as MongoDBRuntimeException; + +/** + * MongoDB cache implementation + */ +class MongoDB extends AbstractCache +{ + use InitializeOption; + + /** + * @var Collection + */ + protected $collection; + + /** + * Class constructor + * + * @param Collection $collection + */ + public function __construct(Collection $collection) + { + $this->collection = $collection; + } + + /** + * Initialize the DB collection. + * Set TTL index. + */ + protected function initialize() + { + $this->collection->createIndex(['ttl' => 1], ['expireAfterSeconds' => 0]); + } + + + /** + * Create the default packer for this cache implementation. + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new MongoDBBinaryPacker(); + } + + /** + * Get filter for key and ttl. + * + * @param string|iterable $key + * @return array + */ + protected function filter($key) + { + if (is_array($key)) { + $key = ['$in' => $key]; + } + + return [ + '_id' => $key, + '$or' => [ + ['ttl' => ['$gt' => new BSONUTCDateTime($this->currentTimestamp() * 1000)]], + ['ttl' => null] + ] + ]; + } + + /** + * {@inheritdoc } + */ + public function get($key, $default = null) + { + $filter = $this->filter($this->keyToId($key)); + + try { + $data = $this->collection->findOne($filter); + } catch (MongoDBRuntimeException $e) { + return $default; + } + + return isset($data) ? $this->unpack($data['value']) : $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $idKeyPairs = $this->mapKeysToIds($keys); + + if (empty($idKeyPairs)) { + return []; + } + + $filter = $this->filter(array_keys($idKeyPairs)); + $items = array_fill_keys(array_values($idKeyPairs), $default); + + try { + $rows = $this->collection->find($filter); + } catch (MongoDBRuntimeException $e) { + return $items; + } + + foreach ($rows as $row) { + $id = $row['_id']; + $key = $idKeyPairs[$id]; + + $items[$key] = $this->unpack($row['value']); + } + + return $items; + } + + /** + * {@inheritdoc } + */ + public function has($key) + { + $filter = $this->filter($this->keyToId($key)); + + try { + $count = $this->collection->count($filter); + } catch (MongoDBRuntimeException $e) { + return false; + } + + return $count > 0; + } + + /** + * {@inheritdoc } + */ + public function set($key, $value, $ttl = null) + { + $id = $this->keyToId($key); + + $item = [ + '_id' => $id, + 'ttl' => $this->getTtlBSON($ttl), + 'value' => $this->pack($value) + ]; + + try { + $this->collection->replaceOne(['_id' => $id], $item, ['upsert' => true]); + } catch (MongoDBRuntimeException $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $this->assertIterable($values, 'values not iterable'); + + if (empty($values)) { + return true; + } + + $bsonTtl = $this->getTtlBSON($ttl); + $items = []; + + foreach ($values as $key => $value) { + $id = $this->keyToId(is_int($key) ? (string)$key : $key); + + $items[] = [ + 'replaceOne' => [ + ['_id' => $id], + [ + '_id' => $id, + 'ttl' => $bsonTtl, + 'value' => $this->pack($value) + ], + [ 'upsert' => true ] + ] + ]; + } + + try { + $this->collection->bulkWrite($items); + } catch (MongoDBRuntimeException $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $id = $this->keyToId($key); + + try { + $this->collection->deleteOne(['_id' => $id]); + } catch (MongoDBRuntimeException $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $idKeyPairs = $this->mapKeysToIds($keys); + + try { + if (!empty($idKeyPairs)) { + $this->collection->deleteMany(['_id' => ['$in' => array_keys($idKeyPairs)]]); + } + } catch (MongoDBRuntimeException $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + try { + $this->collection->drop(); + } catch (MongoDBRuntimeException $e) { + return false; + } + + $this->requireInitialization(); + + return true; + } + + + /** + * Get TTL as Date type BSON object + * + * @param null|int|DateInterval $ttl + * @return BSONUTCDatetime|null + */ + protected function getTtlBSON($ttl): ?BSONUTCDatetime + { + return isset($ttl) ? new BSONUTCDateTime($this->ttlToTimestamp($ttl) * 1000) : null; + } +} diff --git a/src/Mysqli.php b/src/Mysqli.php new file mode 100644 index 0000000..b2b724f --- /dev/null +++ b/src/Mysqli.php @@ -0,0 +1,302 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\Exception\UnexpectedValueException; +use Desarrolla2\Cache\Option\InitializeTrait; +use mysqli as Server; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\SerializePacker; + +/** + * Mysqli cache adapter. + * + * Errors are silently ignored but exceptions are **not** caught. Beware when using `mysqli_report()` to throw a + * `mysqli_sql_exception` on error. + */ +class Mysqli extends AbstractCache +{ + use InitializeTrait; + + /** + * @var Server + */ + protected $server; + + /** + * @var string + */ + protected $table = 'cache'; + + + /** + * Class constructor + * + * @param Server $server + */ + public function __construct(Server $server) + { + $this->server = $server; + } + + + /** + * Initialize table. + * Automatically delete old cache. + */ + protected function initialize() + { + if ($this->initialized !== false) { + return; + } + + $this->query( + "CREATE TABLE IF NOT EXISTS `{table}` " + . "( `key` VARCHAR(255), `value` TEXT, `ttl` INT UNSIGNED, PRIMARY KEY (`key`) )" + ); + + $this->query( + "CREATE EVENT IF NOT EXISTS `apply_ttl_{$this->table}` ON SCHEDULE EVERY 1 HOUR DO BEGIN" + . " DELETE FROM {table} WHERE `ttl` < NOW();" + . " END" + ); + + $this->initialized = true; + } + + /** + * Create the default packer for this cache implementation. + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new SerializePacker(); + } + + + /** + * Set the table name + * + * @param string $table + */ + public function setTableOption(string $table) + { + $this->table = $table; + $this->requireInitialization(); + } + + /** + * Get the table name + * + * @return string + */ + public function getTableOption(): string + { + return $this->table; + } + + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $this->initialize(); + + $result = $this->query( + 'SELECT `value` FROM {table} WHERE `key` = %s AND (`ttl` > %d OR `ttl` IS NULL) LIMIT 1', + $this->keyToId($key), + $this->currentTimestamp() + ); + + $row = $result !== false ? $result->fetch_row() : null; + + return $row ? $this->unpack($row[0]) : $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $idKeyPairs = $this->mapKeysToIds($keys); + + if (empty($idKeyPairs)) { + return []; + } + + $this->initialize(); + + $values = array_fill_keys(array_values($idKeyPairs), $default); + + $result = $this->query( + 'SELECT `key`, `value` FROM {table} WHERE `key` IN (%s) AND (`ttl` > %d OR `ttl` IS NULL)', + array_keys($idKeyPairs), + $this->currentTimestamp() + ); + + while ((list($id, $value) = $result->fetch_row())) { + $key = $idKeyPairs[$id]; + $values[$key] = $this->unpack($value); + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + $this->initialize(); + + $result = $this->query( + 'SELECT COUNT(`key`) FROM {table} WHERE `key` = %s AND (`ttl` > %d OR `ttl` IS NULL) LIMIT 1', + $this->keyToId($key), + $this->currentTimestamp() + ); + + list($count) = $result ? $result->fetch_row() : null; + + return isset($count) && $count > 0; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $this->initialize(); + + $result = $this->query( + 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES (%s, %s, %s)', + $this->keyToId($key), + $this->pack($value), + $this->ttlToTimestamp($ttl) + ); + + return $result !== false; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $this->assertIterable($values, 'values not iterable'); + + if (empty($values)) { + return true; + } + + $this->initialize(); + + $timeTtl = $this->ttlToTimestamp($ttl); + $query = 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES'; + + foreach ($values as $key => $value) { + $query .= sprintf( + ' (%s, %s, %s),', + $this->quote($this->keyToId(is_int($key) ? (string)$key : $key)), + $this->quote($this->pack($value)), + $this->quote($timeTtl) + ); + } + + return $this->query(rtrim($query, ',')) !== false; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $this->initialize(); + + return (bool)$this->query('DELETE FROM {table} WHERE `key` = %s', $this->keyToId($key)); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $idKeyPairs = $this->mapKeysToIds($keys); + + if (empty($idKeyPairs)) { + return true; + } + + $this->initialize(); + + return (bool)$this->query('DELETE FROM {table} WHERE `key` IN (%s)', array_keys($idKeyPairs)); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->initialize(); + return (bool)$this->query('TRUNCATE {table}'); + } + + + /** + * Query the MySQL server + * + * @param string $query + * @param mixed[] $params + * @return \mysqli_result|false + */ + protected function query($query, ...$params) + { + $saveParams = array_map([$this, 'quote'], $params); + + $baseSql = str_replace('{table}', $this->table, $query); + $sql = vsprintf($baseSql, $saveParams); + + $ret = $this->server->query($sql); + + if ($ret === false) { + trigger_error($this->server->error . " $sql", E_USER_NOTICE); + } + + return $ret; + } + + /** + * Quote a value to be used in an array + * + * @param mixed $value + * @return mixed + */ + protected function quote($value) + { + if ($value === null) { + return 'NULL'; + } + + if (is_array($value)) { + return join(', ', array_map([$this, 'quote'], $value)); + } + + return is_string($value) + ? ('"' . $this->server->real_escape_string($value) . '"') + : (is_float($value) ? (float)$value : (int)$value); + } +} diff --git a/src/NotCache.php b/src/NotCache.php new file mode 100644 index 0000000..1aafc7f --- /dev/null +++ b/src/NotCache.php @@ -0,0 +1,93 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractCache; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\NopPacker; + +/** + * Dummy cache handler + */ +class NotCache extends AbstractCache +{ + /** + * Create the default packer for this cache implementation. + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new NopPacker(); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return true; + } +} diff --git a/src/Option/FilenameTrait.php b/src/Option/FilenameTrait.php new file mode 100644 index 0000000..345607c --- /dev/null +++ b/src/Option/FilenameTrait.php @@ -0,0 +1,91 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Option; + +use TypeError; +use Desarrolla2\Cache\File\BasicFilename; + +/** + * Use filename generator + */ +trait FilenameTrait +{ + /** + * @var callable + */ + protected $filename; + + + /** + * Filename format or callable. + * The filename format will be applied using sprintf, replacing `%s` with the key. + * + * @param string|callable $filename + * @return void + */ + protected function setFilenameOption($filename): void + { + if (is_string($filename)) { + $filename = new BasicFilename($filename); + } + + if (!is_callable($filename)) { + throw new TypeError("Filename should be a string or callable"); + } + + $this->filename = $filename; + } + + /** + * Get the filename callable + * + * @return callable + */ + protected function getFilenameOption(): callable + { + if (!isset($this->filename)) { + $this->filename = new BasicFilename('%s.' . $this->getPacker()->getType()); + } + + return $this->filename; + } + + /** + * Create a filename based on the key + * + * @param string|mixed $key + * @return string + */ + protected function getFilename($key): string + { + $id = $this->keyToId($key); + $generator = $this->getFilenameOption(); + + return $this->cacheDir . DIRECTORY_SEPARATOR . $generator($id); + } + + /** + * Get a wildcard for all files + * + * @return string + */ + protected function getWildcard(): string + { + $generator = $this->getFilenameOption(); + + return $this->cacheDir . DIRECTORY_SEPARATOR . $generator(''); + } +} diff --git a/src/Option/InitializeTrait.php b/src/Option/InitializeTrait.php new file mode 100644 index 0000000..680d507 --- /dev/null +++ b/src/Option/InitializeTrait.php @@ -0,0 +1,65 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Option; + +/** + * Auto initialize the cache + */ +trait InitializeTrait +{ + /** + * Is cache initialized + * @var bool|null + */ + protected $initialized = false; + + + /** + * Enable/disable initialization + * + * @param bool $enabled + */ + public function setInitializeOption(bool $enabled) + { + $this->initialized = $enabled ? (bool)$this->initialized : null; + } + + /** + * Should initialize + * + * @return bool + */ + protected function getInitializeOption(): bool + { + return $this->initialized !== null; + } + + /** + * Mark as initialization required (if enabled) + */ + protected function requireInitialization() + { + $this->initialized = isset($this->initialized) ? false : null; + } + + + /** + * Initialize + * + * @return void + */ + abstract protected function initialize(): void; +} diff --git a/src/Option/PrefixTrait.php b/src/Option/PrefixTrait.php new file mode 100644 index 0000000..2befaf4 --- /dev/null +++ b/src/Option/PrefixTrait.php @@ -0,0 +1,49 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Option; + +/** + * Prefix option + */ +trait PrefixTrait +{ + /** + * @var string + */ + protected $prefix = ''; + + + /** + * Set the key prefix + * + * @param string $prefix + * @return void + */ + protected function setPrefixOption(string $prefix): void + { + $this->prefix = $prefix; + } + + /** + * Get the key prefix + * + * @return string + */ + protected function getPrefixOption(): string + { + return $this->prefix; + } +} diff --git a/src/Option/TtlTrait.php b/src/Option/TtlTrait.php new file mode 100644 index 0000000..330aa15 --- /dev/null +++ b/src/Option/TtlTrait.php @@ -0,0 +1,54 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Option; + +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * TTL option + */ +trait TtlTrait +{ + /** + * @var int|null + */ + protected $ttl = null; + + /** + * Set the maximum time to live (ttl) + * + * @param int|null $value Seconds or null to live forever + * @throws InvalidArgumentException + */ + protected function setTtlOption(?int $value): void + { + if (isset($value) && $value < 1) { + throw new InvalidArgumentException('ttl cant be lower than 1'); + } + + $this->ttl = $value; + } + + /** + * Get the maximum time to live (ttl) + * + * @return int|null + */ + protected function getTtlOption(): ?int + { + return $this->ttl; + } +} \ No newline at end of file diff --git a/src/Packer/JsonPacker.php b/src/Packer/JsonPacker.php new file mode 100644 index 0000000..1fa58e8 --- /dev/null +++ b/src/Packer/JsonPacker.php @@ -0,0 +1,69 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Packer; + +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * Pack value through serialization + */ +class JsonPacker implements PackerInterface +{ + /** + * Get cache type (might be used as file extension) + * + * @return string + */ + public function getType() + { + return 'json'; + } + + /** + * Pack the value + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return json_encode($value); + } + + /** + * Unpack the value + * + * @param string $packed + * @return mixed + * @throws InvalidArgumentException + */ + public function unpack($packed) + { + if (!is_string($packed)) { + throw new InvalidArgumentException("packed value should be a string"); + } + + $ret = json_decode($packed); + + if (!isset($ret) && json_last_error()) { + throw new UnexpectedValueException("packed value is not a valid JSON string"); + } + + return $ret; + } +} diff --git a/src/Packer/MongoDBBinaryPacker.php b/src/Packer/MongoDBBinaryPacker.php new file mode 100644 index 0000000..fcd3a83 --- /dev/null +++ b/src/Packer/MongoDBBinaryPacker.php @@ -0,0 +1,77 @@ + + * @author Arnold Daniels + */ + +namespace Desarrolla2\Cache\Packer; + +use Desarrolla2\Cache\Packer\PackerInterface; +use MongoDB\BSON\Binary; + +/** + * Pack as BSON binary + * + * @todo Don't use serialize when packer chain is here. + */ +class MongoDBBinaryPacker implements PackerInterface +{ + /** + * @var array + */ + protected $options; + + /** + * SerializePacker constructor + * + * @param array $options Any options to be provided to unserialize() + */ + public function __construct(array $options = ['allowed_classes' => true]) + { + $this->options = $options; + } + + /** + * Get cache type (might be used as file extension) + * + * @return string + */ + public function getType() + { + return 'bson'; + } + + /** + * Pack the value + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return new Binary(serialize($value), Binary::TYPE_GENERIC); + } + + /** + * Unpack the value + * + * @param string $packed + * @return string + * @throws \UnexpectedValueException if he value can't be unpacked + */ + public function unpack($packed) + { + if (!$packed instanceof Binary) { + throw new InvalidArgumentException("packed value should be BSON binary"); + } + + return unserialize((string)$packed, $this->options); + } +} \ No newline at end of file diff --git a/src/Packer/NopPacker.php b/src/Packer/NopPacker.php new file mode 100644 index 0000000..3e1bd44 --- /dev/null +++ b/src/Packer/NopPacker.php @@ -0,0 +1,57 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Packer; + +use Desarrolla2\Cache\Packer\PackerInterface; + +/** + * Don't pack, just straight passthrough + */ +class NopPacker implements PackerInterface +{ + /** + * Get cache type (might be used as file extension) + * + * @return string + */ + public function getType() + { + return 'data'; + } + + /** + * Pack the value + * + * @param mixed $value + * @return mixed + */ + public function pack($value) + { + return $value; + } + + /** + * Unpack the value + * + * @param mixed $packed + * @return mixed + */ + public function unpack($packed) + { + return $packed; + } +} diff --git a/src/Packer/PackerInterface.php b/src/Packer/PackerInterface.php new file mode 100644 index 0000000..1c1539e --- /dev/null +++ b/src/Packer/PackerInterface.php @@ -0,0 +1,47 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Packer; + +/** + * Interface for packer / unpacker + */ +interface PackerInterface +{ + /** + * Get cache type (might be used as file extension) + * + * @return string + */ + public function getType(); + + /** + * Pack the value + * + * @param mixed $value + * @return string|mixed + */ + public function pack($value); + + /** + * Unpack the value + * + * @param string|mixed $packed + * @return string + * @throws \UnexpectedValueException if the value can't be unpacked + */ + public function unpack($packed); +} diff --git a/src/Packer/PackingTrait.php b/src/Packer/PackingTrait.php new file mode 100644 index 0000000..3c6192f --- /dev/null +++ b/src/Packer/PackingTrait.php @@ -0,0 +1,86 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Packer; + +/** + * Support packing for Caching adapter + */ +trait PackingTrait +{ + /** + * @var PackerInterface + */ + protected $packer; + + + /** + * Create the default packer for this cache implementation + * + * @return PackerInterface + */ + abstract protected static function createDefaultPacker(): PackerInterface; + + /** + * Set a packer to pack (serialialize) and unpack (unserialize) the data. + * + * @param PackerInterface $packer + * @return static + */ + public function withPacker(PackerInterface $packer) + { + $cache = $this->cloneSelf(); + $cache->packer = $packer; + + return $cache; + } + + /** + * Get the packer + * + * @return PackerInterface + */ + protected function getPacker(): PackerInterface + { + if (!isset($this->packer)) { + $this->packer = static::createDefaultPacker(); + } + + return $this->packer; + } + + /** + * Pack the value + * + * @param mixed $value + * @return string|mixed + */ + protected function pack($value) + { + return $this->getPacker()->pack($value); + } + + /** + * Unpack the data to retrieve the value + * + * @param string|mixed $packed + * @return mixed + * @throws UnexpectedValueException + */ + protected function unpack($packed) + { + return $this->getPacker()->unpack($packed); + } +} diff --git a/src/Packer/SerializePacker.php b/src/Packer/SerializePacker.php new file mode 100644 index 0000000..8df68fd --- /dev/null +++ b/src/Packer/SerializePacker.php @@ -0,0 +1,78 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache\Packer; + +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * Pack value through serialization + */ +class SerializePacker implements PackerInterface +{ + /** + * @var array + */ + protected $options; + + /** + * SerializePacker constructor + * + * @param array $options Any options to be provided to unserialize() + */ + public function __construct(array $options = ['allowed_classes' => true]) + { + $this->options = $options; + } + + /** + * Get cache type (might be used as file extension) + * + * @return string + */ + public function getType() + { + return 'php.cache'; + } + + /** + * Pack the value + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return serialize($value); + } + + /** + * Unpack the value + * + * @param string $packed + * @return string + * @throws \UnexpectedValueException if he value can't be unpacked + */ + public function unpack($packed) + { + if (!is_string($packed)) { + throw new InvalidArgumentException("packed value should be a string"); + } + + return unserialize($packed, $this->options); + } +} diff --git a/src/PhpFile.php b/src/PhpFile.php new file mode 100644 index 0000000..a8774e3 --- /dev/null +++ b/src/PhpFile.php @@ -0,0 +1,111 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractFile; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\SerializePacker; +use Desarrolla2\Cache\File\BasicFilename; + +/** + * Cache file as PHP script. + */ +class PhpFile extends AbstractFile +{ + /** + * Create the default packer for this cache implementation. + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new SerializePacker(); + } + + /** + * Get the filename callable + * + * @return callable + */ + protected function getFilenameOption(): callable + { + if (!isset($this->filename)) { + $this->filename = new BasicFilename('%s.php'); + } + + return $this->filename; + } + + /** + * Create a PHP script returning the cached value + * + * @param mixed $value + * @param int|null $ttl + * @return string + */ + public function createScript($value, ?int $ttl): string + { + $macro = var_export($value, true); + + if (strpos($macro, 'stdClass::__set_state') !== false) { + $macro = preg_replace_callback("/('([^'\\\\]++|''\\.)')|stdClass::__set_state/", $macro, function($match) { + return empty($match[1]) ? '(object)' : $match[1]; + }); + } + + return $ttl !== null + ? "getFilename($key); + + if (!file_exists($cacheFile)) { + return $default; + } + + $packed = include $cacheFile; + + return $packed === false ? $default : $this->unpack($packed); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return $this->get($key) !== null; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $cacheFile = $this->getFilename($key); + + $packed = $this->pack($value); + $script = $this->createScript($packed, $this->ttlToTimestamp($ttl)); + + return $this->writeFile($cacheFile, $script); + } +} diff --git a/src/Predis.php b/src/Predis.php new file mode 100644 index 0000000..d5c9b9d --- /dev/null +++ b/src/Predis.php @@ -0,0 +1,245 @@ + + * @author Arnold Daniels + */ + +declare(strict_types=1); + +namespace Desarrolla2\Cache; + +use Desarrolla2\Cache\AbstractCache; +use Desarrolla2\Cache\Exception\UnexpectedValueException; +use Desarrolla2\Cache\Packer\PackerInterface; +use Desarrolla2\Cache\Packer\SerializePacker; +use Predis\Client; +use Predis\Response\ServerException; +use Predis\Response\Status; +use Predis\Response\ErrorInterface; + +/** + * Predis cache adapter. + * + * Errors are silently ignored but ServerExceptions are **not** caught. To PSR-16 compliant disable the `exception` + * option. + */ +class Predis extends AbstractCache +{ + /** + * @var Client + */ + protected $predis; + + /** + * Class constructor + * @see predis documentation about how know your configuration https://github.com/nrk/predis + * + * @param Client $client + */ + public function __construct(Client $client) + { + $this->predis = $client; + } + + /** + * Create the default packer for this cache implementation. + * + * @return PackerInterface + */ + protected static function createDefaultPacker(): PackerInterface + { + return new SerializePacker(); + } + + + /** + * Run a predis command. + * + * @param string $cmd + * @param mixed + * @return mixed|bool + */ + protected function execCommand(string $cmd, ...$args) + { + $command = $this->predis->createCommand($cmd, $args); + $response = $this->predis->executeCommand($command); + + if ($response instanceof ErrorInterface) { + return false; + } + + if ($response instanceof Status) { + return $response->getPayload() === 'OK'; + } + + return $response; + } + + /** + * Set multiple (mset) with expire + * + * @param array $dictionary + * @param int|null $ttlSeconds + * @return bool + */ + protected function msetExpire(array $dictionary, ?int $ttlSeconds): bool + { + if (empty($dictionary)) { + return true; + } + + if (!isset($ttlSeconds)) { + return $this->execCommand('MSET', $dictionary); + } + + $transaction = $this->predis->transaction(); + + foreach ($dictionary as $key => $value) { + $transaction->set($key, $value, 'EX', $ttlSeconds); + } + + try { + $responses = $transaction->execute(); + } catch (ServerException $e) { + return false; + } + + $ok = array_reduce($responses, function($ok, $response) { + return $ok && $response instanceof Status && $response->getPayload() === 'OK'; + }, true); + + return $ok; + } + + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $id = $this->keyToId($key); + $response = $this->execCommand('GET', $id); + + return !empty($response) ? $this->unpack($response) : $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $idKeyPairs = $this->mapKeysToIds($keys); + $ids = array_keys($idKeyPairs); + + $response = $this->execCommand('MGET', $ids); + + if ($response === false) { + return false; + } + + $items = []; + $packedItems = array_combine(array_values($idKeyPairs), $response); + + foreach ($packedItems as $key => $packed) { + $items[$key] = isset($packed) ? $this->unpack($packed) : $default; + } + + return $items; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return $this->execCommand('EXISTS', $this->keyToId($key)); + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $id = $this->keyToId($key); + $packed = $this->pack($value); + + if (!is_string($packed)) { + throw new UnexpectedValueException("Packer must create a string for the data"); + } + + $ttlSeconds = $this->ttlToSeconds($ttl); + + if (isset($ttlSeconds) && $ttlSeconds <= 0) { + return $this->execCommand('DEL', [$id]); + } + + return !isset($ttlSeconds) + ? $this->execCommand('SET', $id, $packed) + : $this->execCommand('SETEX', $id, $ttlSeconds, $packed); + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $this->assertIterable($values, 'values not iterable'); + + $dictionary = []; + + foreach ($values as $key => $value) { + $id = $this->keyToId(is_int($key) ? (string)$key : $key); + $packed = $this->pack($value); + + if (!is_string($packed)) { + throw new UnexpectedValueException("Packer must create a string for the data"); + } + + $dictionary[$id] = $packed; + } + + $ttlSeconds = $this->ttlToSeconds($ttl); + + if (isset($ttlSeconds) && $ttlSeconds <= 0) { + return $this->execCommand('DEL', array_keys($dictionary)); + } + + return $this->msetExpire($dictionary, $ttlSeconds); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $id = $this->keyToId($key); + + return $this->execCommand('DEL', [$id]) !== false; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $ids = array_keys($this->mapKeysToIds($keys)); + + return empty($ids) || $this->execCommand('DEL', $ids) !== false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->execCommand('FLUSHDB'); + } +} diff --git a/tests/AbstractCacheTest.php b/tests/AbstractCacheTest.php new file mode 100644 index 0000000..8c16a7a --- /dev/null +++ b/tests/AbstractCacheTest.php @@ -0,0 +1,93 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Cache\IntegrationTests\SimpleCacheTest; +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * AbstractCacheTest + */ +abstract class AbstractCacheTest extends SimpleCacheTest +{ + /** + * @return array + */ + public function dataProviderForOptions() + { + return [ + ['ttl', 100], + ['prefix', 'test'] + ]; + } + + /** + * @dataProvider dataProviderForOptions + * + * @param string $key + * @param mixed $value + */ + public function testWithOption($key, $value) + { + $cache = $this->cache->withOption($key, $value); + $this->assertEquals($value, $cache->getOption($key)); + + // Check immutability + $this->assertNotSame($this->cache, $cache); + $this->assertNotEquals($value, $this->cache->getOption($key)); + } + + public function testWithOptions() + { + $data = $this->dataProviderForOptions(); + $options = array_combine(array_column($data, 0), array_column($data, 1)); + + $cache = $this->cache->withOptions($options); + + foreach ($options as $key => $value) { + $this->assertEquals($value, $cache->getOption($key)); + } + + // Check immutability + $this->assertNotSame($this->cache, $cache); + + foreach ($options as $key => $value) { + $this->assertNotEquals($value, $this->cache->getOption($key)); + } + } + + + /** + * @return array + */ + public function dataProviderForOptionsException() + { + return [ + ['ttl', 0, InvalidArgumentException::class], + ['foo', 'bar', InvalidArgumentException::class] + ]; + } + + /** + * @dataProvider dataProviderForOptionsException + * + * @param string $key + * @param mixed $value + * @param string $expectedException + */ + public function testWithOptionException($key, $value, $expectedException) + { + $this->expectException($expectedException); + $this->createSimpleCache()->withOption($key, $value); + } +} diff --git a/tests/ApcuCacheTest.php b/tests/ApcuCacheTest.php new file mode 100644 index 0000000..7b96cf4 --- /dev/null +++ b/tests/ApcuCacheTest.php @@ -0,0 +1,44 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Apcu as ApcuCache; + +/** + * ApcuCacheTest + */ +class ApcuCacheTest extends AbstractCacheTest +{ + public static function setUpBeforeClass() + { + // Required to check the TTL for new entries + ini_set('apc.use_request_time', false); + } + + public function createSimpleCache() + { + if (!extension_loaded('apcu')) { + $this->markTestSkipped( + 'The APCu extension is not available.' + ); + } + if (!ini_get('apc.enable_cli')) { + $this->markTestSkipped( + 'You need to enable apc.enable_cli' + ); + } + + return new ApcuCache(); + } +} diff --git a/tests/ChainTest.php b/tests/ChainTest.php new file mode 100644 index 0000000..9397a1c --- /dev/null +++ b/tests/ChainTest.php @@ -0,0 +1,216 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Chain as CacheChain; +use Desarrolla2\Cache\Memory as MemoryCache; + +/** + * ChainTest + */ +class ChainTest extends AbstractCacheTest +{ + public function createSimpleCache() + { + $adapters = [new MemoryCache()]; // For the general PSR-16 tests, we don't need more than 1 adapter + + return new CacheChain($adapters); + } + + + public function tearDown() + { + // No need to clear cache, as the adapters don't persist between tests. + } + + + public function testChainSet() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('set')->with("foo", "bar", 300); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('set')->with("foo", "bar", 300); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $cache->set("foo", "bar", 300); + } + + public function testChainSetMultiple() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('setMultiple')->with(["foo" => 1, "bar" => 2], 300); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('setMultiple')->with(["foo" => 1, "bar" => 2], 300); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $cache->setMultiple(["foo" => 1, "bar" => 2], 300); + } + + + public function testChainGetFirst() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('get')->with("foo")->willReturn("bar"); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->never())->method('get'); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertEquals("bar", $cache->get("foo", 42)); + } + + public function testChainGetSecond() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('get')->with("foo")->willReturn(null); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('get')->with("foo")->willReturn("car"); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertEquals("car", $cache->get("foo", 42)); + } + + public function testChainGetNone() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('get')->with("foo")->willReturn(null); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('get')->with("foo")->willReturn(null); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertEquals(42, $cache->get("foo", 42)); + } + + + public function testChainGetMultipleFirst() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('getMultiple')->with(["foo", "bar"]) + ->willReturn(["foo" => 1, "bar" => 2]); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->never())->method('getMultiple'); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertEquals(["foo" => 1, "bar" => 2], $cache->getMultiple(["foo", "bar"], 42)); + } + + public function testChainGetMultipleMixed() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('getMultiple') + ->with($this->equalTo(["foo", "bar", "wux", "lot"], 0.0, 10, true)) + ->willReturn(["foo" => null, "bar" => 2, "wux" => null, "lot" => null]); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('getMultiple') + ->with($this->equalTo(["foo", "wux", "lot"], 0.0, 10, true)) + ->willReturn(["foo" => 11, "wux" => 15, "lot" => null]); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $expected = ["foo" => 11, "bar" => 2, "wux" => 15, "lot" => 42]; + $this->assertEquals($expected, $cache->getMultiple(["foo", "bar", "wux", "lot"], 42)); + } + + + public function testChainHasFirst() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(true); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->never())->method('has'); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertTrue($cache->has("foo")); + } + + public function testChainHasSecond() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(false); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('has')->with("foo")->willReturn(true); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertTrue($cache->has("foo")); + } + + public function testChainHasNone() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('has')->with("foo")->willReturn(false); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('has')->with("foo")->willReturn(false); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $this->assertFalse($cache->has("foo")); + } + + + public function testChainDelete() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('delete')->with("foo"); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('delete')->with("foo"); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $cache->delete("foo"); + } + + public function testChainDeleteMultiple() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('deleteMultiple')->with(["foo", "bar"]); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('deleteMultiple')->with(["foo", "bar"]); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $cache->deleteMultiple(["foo", "bar"]); + } + + public function testChainClear() + { + $adapter1 = $this->createMock(MemoryCache::class); + $adapter1->expects($this->once())->method('clear'); + + $adapter2 = $this->createMock(MemoryCache::class); + $adapter2->expects($this->once())->method('clear'); + + $cache = new CacheChain([$adapter1, $adapter2]); + + $cache->clear(); + } +} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/AbstractCacheTest.php b/tests/Desarrolla2/Cache/Adapter/Test/AbstractCacheTest.php deleted file mode 100644 index 4b0c705..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/AbstractCacheTest.php +++ /dev/null @@ -1,138 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Symfony\Component\Yaml\Yaml; - -/** - * AbstractCacheTest - */ -abstract class AbstractCacheTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var \Desarrolla2\Cache\Cache - */ - protected $cache; - - /** - * @var array - */ - protected $config = array(); - - public function setup() - { - $configurationFile = realpath(__DIR__.'/../../../../').'/config.yml'; - - if (!is_file($configurationFile)) { - throw new \Exception(' Configuration file not found in "'.$configurationFile.'" '); - } - $this->config = Yaml::parse(file_get_contents($configurationFile)); - } - - /** - * @return array - */ - public function dataProvider() - { - return array( - array('key1', 'value', 1), - array('key2', 'value', 100), - array('key3', 'value', null), - ); - } - - /** - * @return array - */ - public function dataProviderForOptions() - { - return array( - array('ttl', 100), - ); - } - - /** - * - * @dataProvider dataProvider - * @param string $key - * @param mixed $value - * @param int|null $ttl - */ - public function testHash($key, $value, $ttl) - { - $this->assertNull($this->cache->delete($key)); - $this->assertFalse($this->cache->has($key)); - $this->assertNull($this->cache->set($key, $value, $ttl)); - $this->assertTrue($this->cache->has($key)); - } - - /** - * - * @dataProvider dataProvider - * @param string $key - * @param mixed $value - * @param int|null $ttl - */ - public function testGet($key, $value, $ttl) - { - $this->cache->set($key, $value, $ttl); - $this->assertEquals($value, $this->cache->get($key)); - } - - /** - * - * @dataProvider dataProvider - * @param string $key - * @param mixed $value - * @param int|null $ttl - */ - public function testDelete($key, $value, $ttl) - { - $this->cache->set($key, $value, $ttl); - $this->assertNull($this->cache->delete($key)); - $this->assertFalse($this->cache->has($key)); - } - - /** - * @dataProvider dataProviderForOptions - * @param string $key - * @param mixed $value - */ - public function testSetOption($key, $value) - { - $this->assertTrue($this->cache->setOption($key, $value)); - } - - /** - * @dataProvider dataProviderForOptionsException - * @param string $key - * @param mixed $value - * @param \Exception $expectedException - */ - public function testSetOptionException($key, $value, $expectedException) - { - $this->setExpectedException($expectedException); - $this->cache->setOption($key, $value); - } - - public function testHasWithTtlExpired() - { - $key = 'key1'; - $value = 'value1'; - $ttl = 1; - $this->cache->set($key, $value, $ttl); - sleep($ttl + 1); - $this->assertFalse($this->cache->has($key)); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/ApcCacheTest.php b/tests/Desarrolla2/Cache/Adapter/Test/ApcCacheTest.php deleted file mode 100644 index f97f1b2..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/ApcCacheTest.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Apc; - -/** - * ApcCacheTest - */ -class ApcCacheTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - if (!extension_loaded('apc') || !ini_get('apc.enable_cli')) { - $this->markTestSkipped( - 'The APC extension is not available.' - ); - } - $this->cache = new Cache(new Apc()); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\ApcCacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\ApcCacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/FileTest.php b/tests/Desarrolla2/Cache/Adapter/Test/FileTest.php deleted file mode 100644 index 9aac98f..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/FileTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\File; - -/** - * FileTest - */ -class FileTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - $this->cache = new Cache(new File($this->config['file']['dir'])); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\FileCacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\FileCacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/MemCacheTest.php b/tests/Desarrolla2/Cache/Adapter/Test/MemCacheTest.php deleted file mode 100644 index 82609b8..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/MemCacheTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\MemCache; - -/** - * MemCacheTest - */ -class MemCacheTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - if (!extension_loaded('memcached') || !class_exists('\Memcache')) { - $this->markTestSkipped( - 'The Memcache extension is not available.' - ); - } - - $adapter = new MemCache(); - $adapter->addServer('localhost', 11211); - $this->cache = new Cache($adapter); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\CacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\CacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/MemcachedTest.php b/tests/Desarrolla2/Cache/Adapter/Test/MemcachedTest.php deleted file mode 100644 index 7c2aa05..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/MemcachedTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Memcached; - -/** - * MemcachedTest - */ -class MemcachedTest extends AbstractCacheTest { - - public function setUp() { - parent::setup(); - if (!extension_loaded('memcached') || !class_exists('\Memcached')) { - $this->markTestSkipped( - 'The Memcached extension is not available.' - ); - } - - $data = [ - [ - 'host' => 'localhost', - 'port' => 11211, - 'weight' => 0 - ] - ]; - - $adapter = new Memcached($data); - $this->cache = new Cache($adapter); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\CacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\CacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/MemoryTest.php b/tests/Desarrolla2/Cache/Adapter/Test/MemoryTest.php deleted file mode 100644 index 6616689..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/MemoryTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Memory; - -/** - * MemoryTest - */ -class MemoryTest extends AbstractCacheTest -{ - public function setUp() - { - $this->cache = new Cache(new Memory()); - } - - /** - * @return array - */ - public function dataProviderForOptions() - { - return array( - array('ttl', 100), - array('limit', 100), - ); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\CacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\CacheException'), - ); - } - - public function testExceededLimit() - { - $limit = 1; - $this->cache->setOption('limit', $limit); - for ($i = 0; $i <= $limit; $i++) { - $this->cache->set($i, $i); - } - $this->assertFalse($this->cache->has($i)); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/MongoTest.php b/tests/Desarrolla2/Cache/Adapter/Test/MongoTest.php deleted file mode 100644 index 3f12df8..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/MongoTest.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Mongo; - -/** - * MongoTest - */ -class MongoTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - if (!class_exists('Mongo')) { - $this->markTestSkipped( - 'The Mongo extension is not available.' - ); - } - $this->cache = new Cache(new Mongo($this->config['mongo']['dns'])); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\MongoCacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\MongoCacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/MySQLTest.php b/tests/Desarrolla2/Cache/Adapter/Test/MySQLTest.php deleted file mode 100644 index 90f1459..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/MySQLTest.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\MySQL; - -/** - * MySQLTest - */ -class MySQLTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - if (!extension_loaded('mysqlnd')) { - $this->markTestSkipped( - 'The MySQLnd extension is not available.' - ); - } - $this->cache = new Cache( - new MySQL( - $this->config['mysql']['host'], - $this->config['mysql']['user'], - $this->config['mysql']['password'], - $this->config['mysql']['database'], - $this->config['mysql']['port'] - ) - ); - } - - /** - * @return array - */ - public function dataProviderForOptions() - { - return array( - array('ttl', 100), - ); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\CacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\CacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/RedisTest.php b/tests/Desarrolla2/Cache/Adapter/Test/RedisTest.php deleted file mode 100644 index 815272d..0000000 --- a/tests/Desarrolla2/Cache/Adapter/Test/RedisTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Adapter\Test; - -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Redis; - -/** - * RedisTest - */ -class RedisTest extends AbstractCacheTest -{ - public function setUp() - { - parent::setup(); - $this->cache = new Cache( - new Redis() - ); - } - - /** - * @return array - */ - public function dataProviderForOptions() - { - return array( - array('ttl', 100), - ); - } - - /** - * @return array - */ - public function dataProviderForOptionsException() - { - return array( - array('ttl', 0, '\Desarrolla2\Cache\Exception\CacheException'), - array('file', 100, '\Desarrolla2\Cache\Exception\CacheException'), - ); - } -} diff --git a/tests/Desarrolla2/Cache/Test/CacheTest.php b/tests/Desarrolla2/Cache/Test/CacheTest.php deleted file mode 100644 index 0fb09eb..0000000 --- a/tests/Desarrolla2/Cache/Test/CacheTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - -namespace Desarrolla2\Cache\Test; - -use Desarrolla2\Cache\Cache; - -/** - * CacheTest - */ -class CacheTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var \Desarrolla2\Cache\Cache - */ - protected $cache; - - public function setUp() - { - $this->cache = new Cache(); - } - - /** - * @expectedException \Desarrolla2\Cache\Exception\AdapterNotSetException - */ - public function testGetAdapterThrowsException() - { - $this->cache->getAdapter(); - } -} diff --git a/tests/FileTest.php b/tests/FileTest.php new file mode 100644 index 0000000..477d0f2 --- /dev/null +++ b/tests/FileTest.php @@ -0,0 +1,51 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\File as FileCache; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; + +/** + * FileTest + */ +class FileTest extends AbstractCacheTest +{ + /** + * @var vfsStreamDirectory + */ + private $root; + + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes' + ]; + + public function setUp() + { + $this->root = vfsStream::setup('cache'); + + parent::setUp(); + } + + public function createSimpleCache() + { + return new FileCache(vfsStream::url('cache')); + } + + + public function tearDown() + { + // No need to clear all files, as the virtual filesystem is cleared after each test. + } +} diff --git a/tests/FileTrieTest.php b/tests/FileTrieTest.php new file mode 100644 index 0000000..2d7dc44 --- /dev/null +++ b/tests/FileTrieTest.php @@ -0,0 +1,54 @@ + + * @author Arnold Daniels + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\File as FileCache; +use Desarrolla2\Cache\File\TrieFilename; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; + +/** + * FileTest with Trie structure + */ +class FileTrieTest extends AbstractCacheTest +{ + /** + * @var vfsStreamDirectory + */ + private $root; + + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes' + ]; + + public function setUp() + { + $this->root = vfsStream::setup('cache'); + + parent::setUp(); + } + + public function createSimpleCache() + { + return (new FileCache(vfsStream::url('cache'))) + ->withOption('filename', new TrieFilename('%s.php.cache',4)); + } + + + public function tearDown() + { + // No need to clear all files, as the virtual filesystem is cleared after each test. + } +} diff --git a/tests/FileTtlFileTest.php b/tests/FileTtlFileTest.php new file mode 100644 index 0000000..8df80ec --- /dev/null +++ b/tests/FileTtlFileTest.php @@ -0,0 +1,53 @@ + + * @author Arnold Daniels + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\File as FileCache; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; + +/** + * FileTest + */ +class FileTtlFileTest extends AbstractCacheTest +{ + /** + * @var vfsStreamDirectory + */ + private $root; + + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes' + ]; + + public function setUp() + { + $this->root = vfsStream::setup('cache'); + + parent::setUp(); + } + + public function createSimpleCache() + { + return (new FileCache(vfsStream::url('cache'))) + ->withOption('ttl-strategy', 'file'); + } + + + public function tearDown() + { + // No need to clear all files, as the virtual filesystem is cleared after each test. + } +} diff --git a/tests/MemcachedTest.php b/tests/MemcachedTest.php new file mode 100644 index 0000000..ec1cac2 --- /dev/null +++ b/tests/MemcachedTest.php @@ -0,0 +1,53 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Memcached as MemcachedCache; +use Memcached; +use Desarrolla2\Cache\Exception\InvalidArgumentException; + +/** + * MemcachedTest + */ +class MemcachedTest extends AbstractCacheTest +{ + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 250 bytes' + ]; + + public function setUp() + { + if (!extension_loaded('memcached') || !class_exists('\Memcached')) { + $this->markTestSkipped( + 'The Memcached extension is not available.' + ); + } + + parent::setUp(); + } + + public function createSimpleCache() + { + list($host, $port) = explode(':', CACHE_TESTS_MEMCACHED_SERVER) + [1 => 11211]; + + $adapter = new Memcached(); + $adapter->addServer($host, (int)$port); + + if (!$adapter->flush()) { + return $this->markTestSkipped("Unable to flush Memcached; not running?"); + } + + return new MemcachedCache($adapter); + } +} diff --git a/tests/MemoryTest.php b/tests/MemoryTest.php new file mode 100644 index 0000000..fb12e76 --- /dev/null +++ b/tests/MemoryTest.php @@ -0,0 +1,44 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Memory as MemoryCache; + +/** + * MemoryTest + */ +class MemoryTest extends AbstractCacheTest +{ + public function createSimpleCache() + { + return new MemoryCache(); + } + + public function tearDown() + { + // No need to clear cache, as the adapters don't persist between tests. + } + + public function testExceededLimit() + { + $cache = $this->createSimpleCache()->withOption('limit', 1); + + $cache->set('foo', 1); + $this->assertTrue($cache->has('foo')); + + $cache->set('bar', 1); + $this->assertFalse($cache->has('foo')); + $this->assertTrue($cache->has('bar')); + } +} diff --git a/tests/MongoDBTest.php b/tests/MongoDBTest.php new file mode 100644 index 0000000..5336d81 --- /dev/null +++ b/tests/MongoDBTest.php @@ -0,0 +1,57 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\MongoDB as MongoDBCache; +use MongoDB\Client; + +/** + * MongoDBTest + */ +class MongoDBTest extends AbstractCacheTest +{ + /** + * @var Client + */ + protected static $client; + + /** + * Use one client per test, as the MongoDB extension leaves connections open + */ + public static function setUpBeforeClass() + { + if (!extension_loaded('mongodb')) { + return; + } + + self::$client = new Client(CACHE_TESTS_MONGO_DSN); + } + + public function setUp() + { + if (!isset(self::$client)) { + $this->markTestSkipped('The mongodb extension is not available'); + } + + parent::setUp(); + } + + public function createSimpleCache() + { + $collection = self::$client->selectCollection(CACHE_TESTS_MONGO_DATABASE, 'cache'); + + return (new MongoDBCache($collection)) + ->withOption('initialize', false); + } +} diff --git a/tests/MysqliTest.php b/tests/MysqliTest.php new file mode 100644 index 0000000..35bfdf5 --- /dev/null +++ b/tests/MysqliTest.php @@ -0,0 +1,70 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Mysqli as MysqliCache; + +/** + * MysqliTest + */ +class MysqliTest extends AbstractCacheTest +{ + /** + * @var \mysqli + */ + protected $mysqli; + + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 255 bytes' + ]; + + public function setUp() + { + if (!class_exists('mysqli')) { + return $this->markTestSkipped("mysqli extension not loaded"); + } + + try { + $this->mysqli = new \mysqli( + ini_get('mysqli.default_host'), + ini_get('mysqli.default_user') ?: 'root' + ); + } catch (\Exception $e) { + return $this->markTestSkipped("skipping mysqli test; " . mysqli_connect_error()); + } + + $this->mysqli->query('CREATE DATABASE IF NOT EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`'); + $this->mysqli->select_db(CACHE_TESTS_MYSQLI_DATABASE); + + $this->mysqli->query("CREATE TABLE IF NOT EXISTS `cache` " + ."( `key` VARCHAR(255), `value` TEXT, `ttl` INT UNSIGNED, PRIMARY KEY (`key`) )"); + + if ($this->mysqli->error) { + $this->markTestSkipped($this->mysqli->error); + } + + parent::setUp(); + } + + public function createSimpleCache() + { + return (new MysqliCache($this->mysqli)) + ->withOption('initialize', false); + } + + public function tearDown() + { + $this->mysqli->query('DROP DATABASE IF EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`'); + $this->mysqli->close(); + } +} diff --git a/tests/Desarrolla2/Cache/Adapter/Test/NotCacheTest.php b/tests/NotCacheTest.php similarity index 66% rename from tests/Desarrolla2/Cache/Adapter/Test/NotCacheTest.php rename to tests/NotCacheTest.php index 81424dc..375c59f 100644 --- a/tests/Desarrolla2/Cache/Adapter/Test/NotCacheTest.php +++ b/tests/NotCacheTest.php @@ -11,15 +11,15 @@ * @author Daniel González */ -namespace Desarrolla2\Cache\Adapter\Test; +namespace Desarrolla2\Test\Cache; -use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\NotCache; +use Desarrolla2\Cache\NotCache as NotCache; +use PHPUnit\Framework\TestCase; /** * NotCacheTest */ -class NotCacheTest extends \PHPUnit_Framework_TestCase +class NoCacheTest extends TestCase { /** * @var \Desarrolla2\Cache\Cache @@ -28,7 +28,7 @@ class NotCacheTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->cache = new Cache(new NotCache()); + $this->cache = new NotCache(); } /** @@ -56,7 +56,7 @@ public function testHas() public function testGet() { $this->cache->set('key', 'value'); - $this->assertFalse($this->cache->get('key')); + $this->assertFalse($this->cache->get('key', false)); } /** @@ -64,7 +64,7 @@ public function testGet() */ public function testSet() { - $this->assertNull($this->cache->set('key', 'value')); + $this->assertFalse($this->cache->set('key', 'value')); } /** @@ -72,14 +72,17 @@ public function testSet() */ public function testDelete() { - $this->assertNull($this->cache->delete('key')); + $this->assertTrue($this->cache->delete('key')); } /** * @dataProvider dataProvider */ - public function testSetOption() + public function testWithOption() { - $this->cache->setOption('ttl', 3600); + $cache = $this->cache->withOption('ttl', 3600); + $this->assertSame(3600, $cache->getOption('ttl')); + + $this->assertNotSame($this->cache, $cache); } } diff --git a/tests/PhpFileTest.php b/tests/PhpFileTest.php new file mode 100644 index 0000000..6f8c6d9 --- /dev/null +++ b/tests/PhpFileTest.php @@ -0,0 +1,51 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\PhpFile as PhpFileCache; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; + +/** + * FileTest with PhpPacker + */ +class PhpFileTest extends AbstractCacheTest +{ + /** + * @var vfsStreamDirectory + */ + private $root; + + protected $skippedTests = [ + 'testBasicUsageWithLongKey' => 'Only support keys up to 64 bytes' + ]; + + public function setUp() + { + $this->root = vfsStream::setup('cache'); + + parent::setUp(); + } + + public function createSimpleCache() + { + return new PhpFileCache(vfsStream::url('cache')); + } + + + public function tearDown() + { + // No need to clear all files, as the virtual filesystem is cleared after each test. + } +} diff --git a/tests/PredisTest.php b/tests/PredisTest.php new file mode 100644 index 0000000..80f8b68 --- /dev/null +++ b/tests/PredisTest.php @@ -0,0 +1,57 @@ + + */ + +namespace Desarrolla2\Test\Cache; + +use Desarrolla2\Cache\Predis as PredisCache; +use Predis\Client; +use Predis\Connection\ConnectionException; + +/** + * PredisTest + */ +class PredisTest extends AbstractCacheTest +{ + /** + * @var Client + */ + protected $client; + + public function setUp() + { + if (!class_exists('Predis\Client')) { + return $this->markTestSkipped('The predis library is not available'); + } + + try { + $this->client = new Client(CACHE_TESTS_PREDIS_DSN, ['exceptions' => false]); + $this->client->connect(); + } catch (ConnectionException $e) { + return $this->markTestSkipped($e->getMessage()); + } + + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + + $this->client->disconnect(); + } + + public function createSimpleCache() + { + return new PredisCache($this->client); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index f394c44..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ - -$loader = require_once __DIR__.'/../vendor/autoload.php'; -Ladybug\Loader::loadHelpers(); diff --git a/tests/config.yml.dist b/tests/config.yml.dist deleted file mode 100644 index ce88605..0000000 --- a/tests/config.yml.dist +++ /dev/null @@ -1,13 +0,0 @@ -file: - dir: '' -memcached: - host: '127.0.0.1' - port: '11211' -mongo: - dns: 'mongodb://localhost:27017' -mysql: - user: 'root' - password: '' - host: '127.0.0.1' - port: '3306' - database: 'cache' diff --git a/tests/performance/Apc.php b/tests/performance/Apc.php index fca822f..230eb6f 100644 --- a/tests/performance/Apc.php +++ b/tests/performance/Apc.php @@ -14,8 +14,8 @@ require_once __DIR__.'/../bootstrap.php'; use Desarrolla2\Cache\Cache; -use Desarrolla2\Cache\Adapter\Apc; +use Desarrolla2\Cache\Adapter\Apcu; -$cache = new Cache(new Apc()); +$cache = new Cache(new Apcu()); require_once __DIR__.'/common.php'; diff --git a/tests/performance/common.php b/tests/performance/common.php index 4e6cb63..414f718 100644 --- a/tests/performance/common.php +++ b/tests/performance/common.php @@ -11,15 +11,14 @@ * @author Daniel González */ -use Desarrolla2\Timer\Timer; //build test data outside of timing loop -$data = array(); +$data = []; for ($i = 1; $i <= 10000; $i++) { $data[$i] = md5($i); } -$timer = new Timer(); +$timer = new \Desarrolla2\Timer\Timer(new \Desarrolla2\Timer\Formatter\Human()); for ($i = 1; $i <= 10000; $i++) { $cache->set($data[$i], $data[$i], 3600); } @@ -38,12 +37,7 @@ } $timer->mark('10.000 has+get combos'); -$benchmarks = $timer->get(); +$benchmarks = $timer->getAll(); foreach ($benchmarks as $benchmark) { - printf( - "%30s : duration %0.2fms memory %s\n", - $benchmark['text'], - $benchmark['from_previous']*1000, - $benchmark['memory'] - ); + ld($benchmark); }