msd Backup hinzugefügt

This commit is contained in:
aschwarz
2023-07-25 19:16:12 +02:00
parent 50297c1d25
commit 1d3ed789b5
680 changed files with 103120 additions and 0 deletions

View File

@ -0,0 +1,18 @@
use-sort:
group:
- _main
group-type: each
sort-type: alph
sort-direction: asc
header: |
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/

View File

@ -0,0 +1,73 @@
name: PHP
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
run:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
ports:
- 3306:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
redis:
image: redis:6.0
ports:
- 6379:6379
mongo:
image: mongo:4.2-bionic
ports:
- 27017:27017
memcached:
image: memcached:1.6
ports:
- 11211:11211
strategy:
fail-fast: false
matrix:
include:
- php: 7.2
composer: '--prefer-lowest'
desc: "Lowest versions"
- php: 7.4
composer: '--prefer-lowest'
desc: "Lowest versions"
- php: 7.2
- php: 7.3
- php: 7.4
coverage: '--coverage-clover /tmp/clover.xml'
- php: 8.0
name: PHP ${{ matrix.php }} ${{ matrix.desc }}
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
extensions: apcu, mongodb, memcached
ini-values: apc.enable_cli=1,mysqli.default_host=127.0.0.1,mysqli.default_port=3306,mysqli.default_user=root
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: composer update --prefer-dist --no-progress ${{ matrix.composer }}
- name: Run PHPUnit
run: vendor/bin/phpunit ${{ matrix.coverage }}
- name: Upload coverage to Scrutinizer
if: ${{ matrix.coverage }}
run: >
wget https://scrutinizer-ci.com/ocular.phar -O "/tmp/ocular.phar" &&
php "/tmp/ocular.phar" code-coverage:upload --format=php-clover /tmp/clover.xml

View File

@ -0,0 +1,9 @@
/build
/composer.lock
/tests/config.json
/vendor
/.idea
/TODO.md
*~
*.swp
/.phpunit.result.cache

View File

@ -0,0 +1,31 @@
#language: php
checks:
php: true
filter:
excluded_paths:
- tests
build:
nodes:
analysis:
environment:
php:
version: 7.4
pecl_extensions:
- apcu
- mongodb
- memcached
mysql: false
postgresql: false
redis: false
mongodb: false
tests:
override:
- phpcs-run src
-
command: vendor/bin/phpstan analyze --error-format=checkstyle | sed '/^\s*$/d' > phpstan-checkstyle.xml
analysis:
file: phpstan-checkstyle.xml
format: 'general-checkstyle'
- php-scrutinizer-run
tools:
external_code_coverage: true

19
msd/vendor/desarrolla2/cache/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2012-2013 Desarrolla2 - http://desarrolla2.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.

174
msd/vendor/desarrolla2/cache/README.md vendored Normal file
View File

@ -0,0 +1,174 @@
# Desarolla2 Cache
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.
![life-is-hard-cache-is](https://user-images.githubusercontent.com/100821/41566888-ecd60cde-735d-11e8-893f-da42b2cd65e7.jpg)
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-github-actions]][link-github-actions]
[![Coverage Status][ico-coverage]][link-scrutinizer]
[![Quality Score][ico-code-quality]][link-scrutinizer]
[![Total Downloads][ico-downloads]][link-downloads]
[![Today Downloads][ico-today-downloads]][link-downloads]
[![Gitter][ico-gitter]][link-gitter]
## Installation
```
composer require desarrolla2/cache
```
## Usage
``` php
use Desarrolla2\Cache\Memory as Cache;
$cache = new Cache();
$value = $cache->get('key');
if (!isset($value)) {
$value = do_something();
$cache->set('key', $value, 3600);
}
echo $value;
```
## Adapters
* [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.
* [Chain](docs/implementations/chain.md)
[Other implementations][todo-implementations] are planned. Please vote or
provide a PR to speed up the process of adding the to this library.
[todo-implementations]: https://github.com/desarrolla2/Cache/issues?q=is%3Aissue+is%3Aopen+label%3Aadapter
### Options
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
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).
Setting the TTL to 0 or a negative number, means the cache should live forever.
## Methods
Each cache implementation has the following `Psr\SimpleCache\CacheInterface`
methods:
##### `get(string $key [, mixed $default])`
Retrieve the value corresponding to a provided key
##### `has(string $key)`
Retrieve the if value corresponding to a provided key exist
##### `set(string $key, mixed $value [, int $ttl])`
Add a value to the cache under a unique key
##### `delete(string $key)`
Delete a value from the cache
##### `clear()`
Clear all cache
##### `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
##### `deleteMultiple(array $keys)`
Deletes multiple cache items in a single operation
.
The `Desarrolla2\Cache\CacheInterface` also has the following methods:
##### `withOption(string $key, string $value)`
Set option for implementation. Creates a new instance.
##### `withOptions(array $options)`
Set multiple options for implementation. Creates a new instance.
##### `getOption(string $key)`
Get option for implementation.
## Packers
Cache objects typically hold a `Desarrolla2\Cache\Packer\PackerInterface`
object. By default, packing is done using `serialize` and `unserialize`.
Available packers are:
* `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
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.
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.
## Contributors
[![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)
[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-coverage]: https://scrutinizer-ci.com/g/desarrolla2/Cache/badges/coverage.png?b=master
[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
[ico-github-actions]: https://github.com/desarrolla2/Cache/workflows/PHP/badge.svg
[link-packagist]: https://packagist.org/packages/desarrolla2/cache
[link-license]: http://hassankhan.mit-license.org
[link-travis]: https://travis-ci.org/desarrolla2/Cache
[link-github-actions]: https://github.com/desarrolla2/Cache/actions
[link-coveralls]: https://coveralls.io/github/desarrolla2/Cache
[link-scrutinizer]: 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

100
msd/vendor/desarrolla2/cache/build.xml vendored Normal file
View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="Cache" default="build">
<target name="install" depends="composer"
description="Prepare for execution"/>
<target name="build" depends="install, init"
description="Run all tests and build everything"/>
<target name="metrics" depends="build, phpunit, phpdoc, pdepend, phpcs, phpmd"
description="Generate Metrics"/>
<target name="clean"
description="Cleanup build artifacts">
<delete dir="cache"/>
<delete dir="build"/>
</target>
<target name="init" depends="clean"
description="Prepare for build">
<mkdir dir="cache/htmlpurifier"/>
<mkdir dir="build/api"/>
<mkdir dir="build/coverage"/>
<mkdir dir="build/pdepend"/>
<mkdir dir="build/phpcs"/>
<mkdir dir="build/phpmd"/>
</target>
<target name="composer"
description="Composer install">
<exec executable="composer">
<arg value="install"/>
</exec>
</target>
<target name="phpdoc"
description="Generate API documentation using PHPDocumentor">
<exec executable="phpdoc">
<arg value="-d"/>
<arg value="src"/>
<arg value="-t"/>
<arg value="build/api/"/>
</exec>
</target>
<target name="phpunit"
description="Run unit tests using PHPUnit">
<exec executable="phpunit">
<arg value="-c"/>
<arg value="phpunit.xml"/>
</exec>
</target>
<target name="pdepend"
description="Generate software metrics charts using PHP_Depend">
<exec executable="pdepend">
<arg value="--jdepend-chart=build/pdepend/dependencies.svg"/>
<arg value="--overview-pyramid=build/pdepend/overview-pyramid.svg"/>
<arg value="src"/>
</exec>
</target>
<target name="phpcs"
description="Generate coding standard metrics using PHPCS">
<exec executable="phpcs">
<arg value="--standard=PSR2"/>
<arg value="--report-full=build/phpcs/full.txt"/>
<arg value="--report-summary=build/phpcs/sumary.txt"/>
<arg value="src"/>
<arg value="tests"/>
</exec>
</target>
<target name="phpmd"
description="Generate coding metrics for mess code using PHPMD">
<exec executable="phpmd">
<arg value="src"/>
<arg value="text"/>
<arg value="codesize,unusedcode,naming,design,controversial"/>
<arg value="--reportfile"/>
<arg value="build/phpmd/report.txt"/>
</exec>
</target>
<target name="cs" description="">
<parallel>
<exec executable="php-cs-fixer">
<arg line="fix src"/>
</exec>
<exec executable="php-formatter">
<arg line="formatter:header:fix src"/>
</exec>
<exec executable="php-formatter">
<arg line="formatter:use:sort src"/>
</exec>
</parallel>
</target>
</project>

View File

@ -0,0 +1,67 @@
{
"name": "desarrolla2/cache",
"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/",
"authors": [
{
"name": "Daniel González",
"homepage": "http://desarrolla2.com/"
},
{
"name": "Arnold Daniels",
"homepage": "https://jasny.net/"
}
],
"provide": {
"psr/simple-cache-implementation": "1.0"
},
"require": {
"php": ">=7.2.0",
"psr/simple-cache": "^1.0"
},
"require-dev": {
"ext-apcu": "*",
"ext-json": "*",
"ext-mysqli": "*",
"ext-memcached": "*",
"predis/predis": "~1.0.0",
"mongodb/mongodb": "^1.3",
"cache/integration-tests": "dev-master",
"phpunit/phpunit": "^8.3 || ^9.0",
"phpstan/phpstan": "^0.12.29",
"symfony/phpunit-bridge": "^5.2",
"mikey179/vfsstream": "v1.6.8"
},
"autoload": {
"psr-4": {
"Desarrolla2\\Cache\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Desarrolla2\\Test\\Cache\\": "tests/"
}
},
"scripts": {
"test": [
"phpstan analyse",
"phpunit --colors=always",
"phpcs -p src"
]
}
}

View File

@ -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.

View File

@ -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`

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
<?php
use Desarrolla2\Cache\Mongo as MongoCache;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$database = $client->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.

View File

@ -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
<?php
use Desarrolla2\Cache\Mysqli as MysqliCache;
$db = new mysqli('localhost');
$cache = new MysqliCache($db);
```
### 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 [scheduled event](https://dev.mysql.com/doc/refman/5.7/en/event-scheduler.html).
```
DELIMITER ;;
CREATE TABLE IF NOT EXISTS `cache` (`key` VARCHAR(255), `value` TEXT, `ttl` INT UNSIGNED, PRIMARY KEY (`key`));;
CREATE EVENT `apply_ttl_cache` ON SCHEDULE 1 HOUR
DO BEGIN
DELETE FROM `cache` WHERE `ttl` < NOW();
END;;
```
In production it's better to disable auto-initialization and create the event
explicitly when setting up the database. This prevents a `CREATE TABLE` and
`CREATE EVENT` query on each request.
### Packer
By default the [`SerializePacker`](../packers/serialize.md) is used.

View File

@ -0,0 +1,13 @@
# NotCache
A [Null object](https://sourcemaking.com/design_patterns/null_object) that
correctly implements the PSR-16 interface, but does not actually cache
anything.
``` php
use Desarrolla2\Cache\NotCache;
$cache = new NotCache();
```
It doesn't use any options or packers.

View File

@ -0,0 +1,74 @@
# PhpFile
Save the cache as PHP script to on the filesystem using
[`var_export`](http://php.net/manual/en/function.var-export.php) when storing
cache and [`include`](http://php.net/manual/en/function.include.php) when
loading cache.
The implementation leverages the PHP engines in-memory file caching (opcache)
to cache application data in addition to code. This method is particularly fast
in PHP7.2+ due to opcode cache optimizations.
PHP file caching should primarily be used for arrays and objects. There is no
performance benefit over APCu for storing strings.
[read more][]
``` php
use Desarrolla2\Cache\PhpFile as PhpFileCache;
$cache = new PhpFileCache();
```
### Options
| name | type | default | |
| --------- | ------------------ | -------------- | ------------------------------------- |
| ttl | int | null | Maximum time to live in seconds |
| prefix | string | "" | Key prefix |
| filename | string or callable | "%s.php" | Filename as sprintf format |
#### Filename option
The `filename` will be parsed using `sprintf` where '%s' is substituted with
the key.
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', 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
### 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

View File

@ -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.

View File

@ -0,0 +1,40 @@
# Performance test
Here are my performance tests, you can view the results ordered from faster to slower.
| Adapter | 10.000 set | 10.000 has | 10.000 get |
| :-------------- | -----------: | -----------: | ---------: |
| NoCache | 0.0637 | 0.0482 | 0.0488 |
| Apcu | 0.0961 | 0.0556 | 0.0770 |
| File | 0.6881 | 0.3426 | 0.3107 |
| Mongo | 13.8144 | 30.0203 | 24.4214 |
## how i run the test?
The test its the same for all Adapters and look like this.
``` php
<?php
$timer = new Timer();
for ($i = 1; $i <= 10000; $i++) {
$cache->set(md5($i), md5($i), 3600);
}
$timer->mark('10.000 set');
for ($i = 1; $i <= 10000; $i++) {
$cache->has(md5($i));
}
$timer->mark('10.000 has');
for ($i = 1; $i <= 10000; $i++) {
$cache->get(md5($i));
}
$timer->mark('10.000 get');
```
if you want run the tests them execute.
``` sh
php test/performance/AdapterName.php
```

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<ruleset name="PSR">
<!-- Include the whole PSR-1 standard -->
<rule ref="PSR1"/>
<!-- Include the whole PSR-2 standard -->
<rule ref="PSR2"/>
</ruleset>

View File

@ -0,0 +1,8 @@
parameters:
level: 3
paths:
- src
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- /^Variable property access/

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="./vendor/autoload.php"
convertWarningsToExceptions="true"
convertNoticesToExceptions="true"
convertErrorsToExceptions="true"
backupStaticAttributes="false"
processIsolation="false"
stopOnFailure="false"
backupGlobals="false"
colors="true"
>
<testsuites>
<testsuite name="Desarrolla2 Cache test suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
<php>
<!--ini name="mysqli.default_host" value="localhost" /-->
<!--ini name="mysqli.default_user" value="root" /-->
<!--ini name="mysqli.default_pw" value=""/-->
<const name="CACHE_TESTS_MYSQLI_DATABASE" value="cache_tests" />
<const name="CACHE_TESTS_MONGO_DSN" value="mongodb://localhost:27017" />
<const name="CACHE_TESTS_MONGO_DATABASE" value="cache_tests" />
<const name="CACHE_TESTS_MEMCACHED_SERVER" value="localhost:11211" />
<const name="CACHE_TESTS_PREDIS_DSN" value="tcp://localhost:6379" />
</php>
</phpunit>

View File

@ -0,0 +1,296 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
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\InvalidArgumentException;
use DateTimeImmutable;
use DateInterval;
use Traversable;
/**
* 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");
}
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
use Desarrolla2\Cache\Option\FilenameTrait as FilenameOption;
/**
* 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
*/
public function __construct(?string $cacheDir = null)
{
if (!$cacheDir) {
$cacheDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'cache';
if(!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
}
$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 '*'");
}
}
/**
* Get the contents of the cache file.
*
* @param string $cacheFile
* @return string
*/
protected function readFile(string $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);
}
/**
* Remove all files from a directory.
*/
protected function removeFiles(string $dir): bool
{
$success = true;
$generator = $this->getFilenameOption();
$objects = $this->streamSafeGlob($dir, $generator('*'));
foreach ($objects as $object) {
$success = $this->deleteFile($object) && $success;
}
return $success;
}
/**
* Recursive delete an empty directory.
*
* @param string $dir
*/
protected function removeRecursively(string $dir): bool
{
$success = $this->removeFiles($dir);
$objects = $this->streamSafeGlob($dir, '*');
foreach ($objects as $object) {
if (!is_dir($object)) {
continue;
}
if (is_link($object)) {
unlink($object);
} else {
$success = $this->removeRecursively($object) && $success;
rmdir($object);
}
}
return $success;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$cacheFile = $this->getFilename($key);
return $this->deleteFile($cacheFile);
}
/**
* Delete cache directory.
*
* {@inheritdoc}
*/
public function clear()
{
$this->removeRecursively($this->cacheDir);
return true;
}
/**
* Glob that is safe with streams (vfs for example)
*
* @param string $directory
* @param string $filePattern
* @return array
*/
protected function streamSafeGlob(string $directory, string $filePattern): array
{
$filePattern = basename($filePattern);
$files = scandir($directory);
$found = [];
foreach ($files as $filename) {
if (in_array($filename, ['.', '..'])) {
continue;
}
if (fnmatch($filePattern, $filename) || fnmatch($filePattern . '.ttl', $filename)) {
$found[] = "{$directory}/{$filename}";
}
}
return $found;
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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();
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Psr\SimpleCache\CacheInterface as PsrCacheInterface;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\KeyMaker\KeyMakerInterface;
/**
* CacheInterface
*/
interface CacheInterface extends PsrCacheInterface
{
/**
* Set option for cache
*
* @param string $key
* @param mixed $value
* @return static
*/
public function withOption(string $key, $value);
/**
* Set multiple options for cache
*
* @param array $options
* @return static
*/
public function withOptions(array $options);
/**
* Get option for cache
*
* @param string $key
* @return mixed
*/
public function getOption($key);
/**
* Set the packer
*
* @param PackerInterface $packer
* @return static
*/
public function withPacker(PackerInterface $packer);
}

View File

@ -0,0 +1,192 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Packer\NopPacker;
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 = [];
foreach ($adapter->getMultiple($missing) as $key => $value) {
if (isset($value)) {
$found[$key] = $value;
}
}
$values = array_merge($values, $found);
$missing = array_values(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;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Exception bad method calls
*/
class BadMethodCallException extends \BadMethodCallException implements PsrCacheException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Interface used for all types of exceptions thrown by the implementing library.
*/
class CacheException extends \RuntimeException implements PsrCacheException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException;
/**
* Exception for invalid cache arguments.
*/
class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Exception for unexpected values when reading from cache.
*/
class UnexpectedValueException extends \UnexpectedValueException implements PsrCacheException
{
}

View File

@ -0,0 +1,175 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
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 string
*/
protected function getTtlStrategyOption(): string
{
return $this->ttlStrategy;
}
/**
* 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($cacheFile) > 0 ? filemtime($cacheFile) + $this->ttl : PHP_INT_MAX;
}
throw new \InvalidArgumentException("Invalid TTL strategy '{$this->ttlStrategy}'");
}
/**
* Set the TTL using one of the strategies
*
* @param int|null $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 ($expiration !== null) {
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);
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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();
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,218 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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 MemcachedServer
*/
protected $server;
/**
* @param MemcachedServer $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);
}
}

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,273 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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(): void
{
$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;
}
}

View File

@ -0,0 +1,312 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
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(): void
{
if ($this->initialized !== false) {
return;
}
$this->query(
"CREATE TABLE IF NOT EXISTS `{table}` "
. "( `key` VARCHAR(255), `value` BLOB, `ttl` BIGINT 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` < UNIX_TIMESTAMP();"
. " 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` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
'si',
$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);
$placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
$paramTypes = str_repeat('s', count($idKeyPairs)) . 'i';
$params = array_keys($idKeyPairs);
$params[] = $this->currentTimestamp();
$result = $this->query(
"SELECT `key`, `value` FROM {table} WHERE `key` IN ($placeholders) AND (`ttl` > ? OR `ttl` IS NULL)",
$paramTypes,
...$params
);
while (([$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` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
'si',
$this->keyToId($key),
$this->currentTimestamp()
);
[$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 (?, ?, ?)',
'ssi',
$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();
$count = 0;
$params = [];
$timeTtl = $this->ttlToTimestamp($ttl);
foreach ($values as $key => $value) {
$count++;
$params[] = $this->keyToId(is_int($key) ? (string)$key : $key);
$params[] = $this->pack($value);
$params[] = $timeTtl;
}
$query = 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES '
. rtrim(str_repeat('(?, ?, ?), ', $count), ', ');
return (bool)$this->query($query, str_repeat('ssi', $count), ...$params);
}
/**
* {@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();
$placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
$paramTypes = str_repeat('s', count($idKeyPairs));
return (bool)$this->query(
"DELETE FROM {table} WHERE `key` IN ($placeholders)",
$paramTypes,
...array_keys($idKeyPairs)
);
}
/**
* {@inheritdoc}
*/
public function clear()
{
$this->initialize();
return (bool)$this->query('TRUNCATE {table}');
}
/**
* Query the MySQL server
*
* @param string $query
* @param string $types
* @param mixed[] $params
* @return \mysqli_result|bool
*/
protected function query($query, $types = '', ...$params)
{
$sql = str_replace('{table}', $this->table, $query);
if ($params === []) {
$ret = $this->server->query($sql);
} else {
$statement = $this->server->prepare($sql);
if ($statement !== false) {
$statement->bind_param($types, ...$params);
$ret = $statement->execute();
$ret = $ret ? ($statement->get_result() ?: true) : false;
} else {
$ret = false;
}
}
if ($this->server->error) {
trigger_error($this->server->error . " $sql", E_USER_NOTICE);
}
return $ret;
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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('');
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}

View File

@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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;
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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);
}

View File

@ -0,0 +1,86 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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);
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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);
}
}

View File

@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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
? "<?php return time() < {$ttl} ? {$macro} : false;"
: "<?php return {$macro};";
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$cacheFile = $this->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);
}
}

View File

@ -0,0 +1,245 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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 ...$args
* @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');
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
namespace Desarrolla2\Test\Cache;
use Desarrolla2\Cache\Apcu as ApcuCache;
/**
* ApcuCacheTest
*/
class ApcuCacheTest extends AbstractCacheTest
{
public static function setUpBeforeClass(): void
{
// 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();
}
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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(): void
{
// 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"]));
}
public function testChainGetMultipleMixed()
{
$adapter1 = $this->createMock(MemoryCache::class);
$adapter1->expects($this->once())->method('getMultiple')
->with($this->equalTo(["foo", "bar", "wux", "lot"]))
->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"]))
->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();
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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 createSimpleCache()
{
$this->root = vfsStream::setup('cache');
return new FileCache(vfsStream::url('cache'));
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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 createSimpleCache()
{
$this->root = vfsStream::setup('cache');
return (new FileCache(vfsStream::url('cache')))
->withOption('filename', new TrieFilename('%s.php.cache',4));
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
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 createSimpleCache()
{
$this->root = vfsStream::setup('cache');
return (new FileCache(vfsStream::url('cache')))
->withOption('ttl-strategy', 'file');
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
namespace Desarrolla2\Test\Cache;
use Desarrolla2\Cache\Memcached as MemcachedCache;
use Memcached;
/**
* MemcachedTest
*/
class MemcachedTest extends AbstractCacheTest
{
protected $skippedTests = [
'testBasicUsageWithLongKey' => 'Only support keys up to 250 bytes'
];
public function createSimpleCache()
{
if (!extension_loaded('memcached') || !class_exists('\Memcached')) {
$this->markTestSkipped(
'The Memcached extension is not available.'
);
}
list($host, $port) = explode(':', CACHE_TESTS_MEMCACHED_SERVER) + [1 => 11211];
$adapter = new Memcached();
$adapter->addServer($host, (int)$port);
if (!$adapter->flush()) {
$this->markTestSkipped("Unable to flush Memcached; not running?");
}
return new MemcachedCache($adapter);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
namespace Desarrolla2\Test\Cache;
use Desarrolla2\Cache\Memory as MemoryCache;
/**
* MemoryTest
*/
class MemoryTest extends AbstractCacheTest
{
public function createSimpleCache()
{
return new MemoryCache();
}
public function tearDown(): void
{
// 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'));
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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(): void
{
if (!extension_loaded('mongodb')) {
return;
}
self::$client = new Client(CACHE_TESTS_MONGO_DSN);
self::$client->listDatabases(); // Fail if unable to connect
}
public function createSimpleCache()
{
if (!isset(self::$client)) {
$this->markTestSkipped('The mongodb extension is not available');
}
$collection = self::$client->selectCollection(CACHE_TESTS_MONGO_DATABASE, 'cache');
return (new MongoDBCache($collection))
->withOption('initialize', false);
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
namespace Desarrolla2\Test\Cache;
use Desarrolla2\Cache\Mysqli as MysqliCache;
/**
* MysqliTest
*/
class MysqliTest extends AbstractCacheTest
{
/**
* @var \mysqli
*/
protected static $mysqli;
protected $skippedTests = [
'testBasicUsageWithLongKey' => 'Only support keys up to 255 bytes'
];
public static function setUpBeforeClass(): void
{
if (class_exists('mysqli')) {
static::$mysqli = new \mysqli(
ini_get('mysqli.default_host') ?: 'localhost',
ini_get('mysqli.default_user') ?: 'root'
);
}
}
public function init(): void
{
if (!class_exists('mysqli')) {
$this->markTestSkipped("mysqli extension not loaded");
}
try {
static::$mysqli->query('CREATE DATABASE IF NOT EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`');
static::$mysqli->select_db(CACHE_TESTS_MYSQLI_DATABASE);
static::$mysqli->query("CREATE TABLE IF NOT EXISTS `cache` "
."( `key` VARCHAR(255), `value` BLOB, `ttl` INT UNSIGNED, PRIMARY KEY (`key`) )");
} catch (\Exception $e) {
$this->markTestSkipped("skipping mysqli test; " . $e->getMessage());
}
if (static::$mysqli->error) {
$this->markTestSkipped(static::$mysqli->error);
}
}
public function createSimpleCache()
{
$this->init();
return (new MysqliCache(static::$mysqli))
->withOption('initialize', false);
}
public static function tearDownAfterClass(): void
{
static::$mysqli->query('DROP DATABASE IF EXISTS `' . CACHE_TESTS_MYSQLI_DATABASE . '`');
static::$mysqli->close();
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
namespace Desarrolla2\Test\Cache;
use Desarrolla2\Cache\NotCache as NotCache;
use PHPUnit\Framework\TestCase;
/**
* NotCacheTest
*/
class NotCacheTest extends TestCase
{
/**
* @var \Desarrolla2\Cache\NotCache
*/
protected $cache;
public function setUp(): void
{
$this->cache = new NotCache();
}
/**
* @return array
*/
public function dataProvider()
{
return array(
array(),
);
}
/**
* @dataProvider dataProvider
*/
public function testHas()
{
$this->cache->set('key', 'value');
$this->assertFalse($this->cache->has('key'));
}
/**
* @dataProvider dataProvider
*/
public function testGet()
{
$this->cache->set('key', 'value');
$this->assertFalse($this->cache->get('key', false));
}
/**
* @dataProvider dataProvider
*/
public function testSet()
{
$this->assertFalse($this->cache->set('key', 'value'));
}
/**
* @dataProvider dataProvider
*/
public function testDelete()
{
$this->assertTrue($this->cache->delete('key'));
}
/**
* @dataProvider dataProvider
*/
public function testWithOption()
{
$cache = $this->cache->withOption('ttl', 3600);
$this->assertSame(3600, $cache->getOption('ttl'));
$this->assertNotSame($this->cache, $cache);
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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 createSimpleCache()
{
$this->root = vfsStream::setup('cache');
return new PhpFileCache(vfsStream::url('cache'));
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
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 createSimpleCache()
{
if (!class_exists('Predis\Client')) {
$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) {
$this->markTestSkipped($e->getMessage());
}
return new PredisCache($this->client);
}
public function tearDown(): void
{
parent::tearDown();
$this->client->disconnect();
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
require_once __DIR__.'/../bootstrap.php';
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter\Apcu;
$cache = new Cache(new Apcu());
require_once __DIR__.'/common.php';

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
require_once __DIR__.'/../bootstrap.php';
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter\File;
$cache = new Cache(new File('/tmp'));
require_once __DIR__.'/common.php';

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
require_once __DIR__.'/../bootstrap.php';
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter\Mongo;
$cache = new Cache(new Mongo('mongodb://localhost:27017'));
require_once __DIR__.'/common.php';

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
require_once __DIR__.'/../bootstrap.php';
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter\NotCache;
$cache = new Cache(new NotCache());
require_once __DIR__.'/common.php';

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
*/
//build test data outside of timing loop
$data = [];
for ($i = 1; $i <= 10000; $i++) {
$data[$i] = md5($i);
}
$timer = new \Desarrolla2\Timer\Timer(new \Desarrolla2\Timer\Formatter\Human());
for ($i = 1; $i <= 10000; $i++) {
$cache->set($data[$i], $data[$i], 3600);
}
$timer->mark('10.000 set');
for ($i = 1; $i <= 10000; $i++) {
$cache->has($data[$i]);
}
$timer->mark('10.000 has');
for ($i = 1; $i <= 10000; $i++) {
$cache->get($data[$i]);
}
$timer->mark('10.000 get');
for ($i = 1; $i <= 10000; $i++) {
$cache->has($data[$i]);
$cache->get($data[$i]);
}
$timer->mark('10.000 has+get combos');
$benchmarks = $timer->getAll();
foreach ($benchmarks as $benchmark) {
ld($benchmark);
}